Cleanup and document panucci.util
[panucci.git] / src / panucci / main.py
blobb62994055fddda440ac5d3d7a8d52163ef5c25e1
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 import panucci
39 from panucci import widgets
40 from panucci import util
41 from panucci import platform
43 log = logging.getLogger('panucci.panucci')
45 try:
46 import pynotify
47 pynotify.init('Panucci')
48 have_pynotify = True
49 except:
50 have_pynotify = False
52 try:
53 import hildon
54 except:
55 if platform.MAEMO:
56 log.critical( 'Using GTK widgets, install "python2.5-hildon" '
57 'for this to work properly.' )
59 from panucci.settings import settings
60 from panucci.player import player
61 from panucci.dbusinterface import interface
62 from panucci.services import ObservableService
64 about_name = 'Panucci'
65 about_text = _('Resuming audiobook and podcast player')
66 about_authors = ['Thomas Perl', 'Nick (nikosapi)', 'Matthew Taylor']
67 about_website = 'http://panucci.garage.maemo.org/'
69 coverart_sizes = {
70 'normal' : 110,
71 'maemo' : 200,
72 'maemo fullscreen' : 275,
75 gtk.icon_size_register('panucci-button', 32, 32)
77 def find_image(filename):
78 locations = ['./icons/', '../icons/', '/usr/share/panucci/',
79 os.path.dirname(sys.argv[0])+'/../icons/']
81 for location in locations:
82 if os.path.exists(location+filename):
83 return os.path.abspath(location+filename)
85 def generate_image(filename, is_stock=False):
86 image = None
87 if is_stock:
88 image = gtk.image_new_from_stock(
89 filename, gtk.icon_size_from_name('panucci-button') )
90 else:
91 filename = find_image(filename)
92 if filename is not None:
93 image = gtk.image_new_from_file(filename)
94 if image is not None:
95 if platform.MAEMO:
96 image.set_padding(20, 20)
97 else:
98 image.set_padding(5, 5)
99 image.show()
100 return image
102 def image(widget, filename, is_stock=False):
103 child = widget.get_child()
104 if child is not None:
105 widget.remove(child)
106 image = generate_image(filename, is_stock)
107 if image is not None:
108 widget.add(image)
110 def dialog( toplevel_window, title, question, description,
111 affirmative_button=gtk.STOCK_YES, negative_button=gtk.STOCK_NO,
112 abortion_button=gtk.STOCK_CANCEL ):
114 """Present the user with a yes/no/cancel dialog.
115 The return value is either True, False or None, depending on which
116 button has been pressed in the dialog:
118 affirmative button (default: Yes) => True
119 negative button (defaut: No) => False
120 abortion button (default: Cancel) => None
122 When the dialog is closed with the "X" button in the window manager
123 decoration, the return value is always None (same as abortion button).
125 You can set any of the affirmative_button, negative_button or
126 abortion_button values to "None" to hide the corresponding action.
128 dlg = gtk.MessageDialog( toplevel_window, gtk.DIALOG_MODAL,
129 gtk.MESSAGE_QUESTION, message_format=question )
131 dlg.set_title(title)
133 if abortion_button is not None:
134 dlg.add_button(abortion_button, gtk.RESPONSE_CANCEL)
135 if negative_button is not None:
136 dlg.add_button(negative_button, gtk.RESPONSE_NO)
137 if affirmative_button is not None:
138 dlg.add_button(affirmative_button, gtk.RESPONSE_YES)
140 dlg.format_secondary_text(description)
142 response = dlg.run()
143 dlg.destroy()
145 if response == gtk.RESPONSE_YES:
146 return True
147 elif response == gtk.RESPONSE_NO:
148 return False
149 elif response in [gtk.RESPONSE_CANCEL, gtk.RESPONSE_DELETE_EVENT]:
150 return None
152 def get_file_from_filechooser(
153 toplevel_window, folder=False, save_file=False, save_to=None):
155 if folder:
156 open_action = gtk.FILE_CHOOSER_ACTION_SELECT_FOLDER
157 else:
158 open_action = gtk.FILE_CHOOSER_ACTION_OPEN
160 if platform.MAEMO:
161 if save_file:
162 args = ( toplevel_window, gtk.FILE_CHOOSER_ACTION_SAVE )
163 else:
164 args = ( toplevel_window, open_action )
166 dlg = hildon.FileChooserDialog( *args )
167 else:
168 if save_file:
169 args = ( _('Select file to save playlist to'), None,
170 gtk.FILE_CHOOSER_ACTION_SAVE,
171 (( gtk.STOCK_CANCEL, gtk.RESPONSE_REJECT,
172 gtk.STOCK_SAVE, gtk.RESPONSE_OK )) )
173 else:
174 args = ( _('Select podcast or audiobook'), None, open_action,
175 (( gtk.STOCK_CANCEL, gtk.RESPONSE_REJECT,
176 gtk.STOCK_OPEN, gtk.RESPONSE_OK )) )
178 dlg = gtk.FileChooserDialog(*args)
180 current_folder = os.path.expanduser(settings.last_folder)
182 if current_folder is not None and os.path.isdir(current_folder):
183 dlg.set_current_folder(current_folder)
185 if save_file and save_to is not None:
186 dlg.set_current_name(save_to)
188 if dlg.run() == gtk.RESPONSE_OK:
189 filename = dlg.get_filename()
190 settings.last_folder = dlg.get_current_folder()
191 else:
192 filename = None
194 dlg.destroy()
195 return filename
197 def set_stock_button_text( button, text ):
198 alignment = button.get_child()
199 hbox = alignment.get_child()
200 image, label = hbox.get_children()
201 label.set_text(text)
203 ##################################################
204 # PanucciGUI
205 ##################################################
206 class PanucciGUI(object):
207 """ The object that holds the entire panucci gui """
209 def __init__(self, filename=None):
210 self.__log = logging.getLogger('panucci.panucci.PanucciGUI')
211 interface.register_gui(self)
213 # Build the base ui (window and menubar)
214 if platform.MAEMO:
215 self.app = hildon.Program()
216 window = hildon.Window()
217 self.app.add_window(window)
218 else:
219 window = gtk.Window(gtk.WINDOW_TOPLEVEL)
221 self.main_window = window
222 window.set_title('Panucci')
223 self.window_icon = find_image('panucci.png')
224 if self.window_icon is not None:
225 window.set_icon_from_file( self.window_icon )
226 window.set_default_size(400, -1)
227 window.set_border_width(0)
228 window.connect("destroy", self.destroy)
230 # Add the tabs (they are private to prevent us from trying to do
231 # something like gui_root.player_tab.some_function() from inside
232 # playlist_tab or vice-versa)
233 self.__player_tab = PlayerTab(self)
234 self.__playlist_tab = PlaylistTab(self)
236 self.playlist_window = gtk.Window(gtk.WINDOW_TOPLEVEL)
237 self.playlist_window.connect('delete-event', gtk.Widget.hide_on_delete)
238 self.playlist_window.set_title(_('Panucci playlist'))
239 self.playlist_window.set_transient_for(self.main_window)
240 self.playlist_window.add(self.__playlist_tab)
242 self.create_actions()
244 if platform.MAEMO:
245 window.set_menu(self.create_menu())
246 window.add(self.__player_tab)
247 else:
248 menu_vbox = gtk.VBox()
249 menu_vbox.set_spacing(0)
250 window.add(menu_vbox)
251 menu_bar = gtk.MenuBar()
252 self.create_desktop_menu(menu_bar)
253 menu_vbox.pack_start(menu_bar, False, False, 0)
254 menu_bar.show()
255 menu_vbox.pack_end(self.__player_tab, True, True, 6)
257 # Tie it all together!
258 self.__ignore_queue_check = False
259 self.__window_fullscreen = False
261 if platform.MAEMO and interface.headset_device is not None:
262 # Enable play/pause with headset button
263 interface.headset_device.connect_to_signal(
264 'Condition', self.handle_headset_button )
266 self.main_window.connect('key-press-event', self.on_key_press)
267 player.playlist.register( 'file_queued', self.on_file_queued )
269 player.playlist.register( 'playlist-to-be-overwritten',
270 self.check_queue )
271 self.__player_tab.register( 'select-current-item-request',
272 self.__select_current_item )
274 self.main_window.show_all()
276 # this should be done when the gui is ready
277 player.init(filepath=filename)
279 def create_actions(self):
280 self.action_open = gtk.Action('open', _('Open'), _('Open a file or playlist'), gtk.STOCK_OPEN)
281 self.action_open.connect('activate', self.open_file_callback)
282 self.action_save = gtk.Action('save', _('Save playlist'), _('Save current playlist to file'), gtk.STOCK_SAVE_AS)
283 self.action_save.connect('activate', self.save_to_playlist_callback)
284 self.action_playlist = gtk.Action('playlist', _('Playlist'), _('Open the current playlist'), None)
285 self.action_playlist.connect('activate', lambda a: self.playlist_window.show())
286 self.action_about = gtk.Action('about', _('About Panucci'), _('Show application version'), gtk.STOCK_ABOUT)
287 self.action_about.connect('activate', self.about_callback)
288 self.action_quit = gtk.Action('quit', _('Quit'), _('Close Panucci'), gtk.STOCK_QUIT)
289 self.action_quit.connect('activate', self.destroy)
291 def create_desktop_menu(self, menu_bar):
292 file_menu_item = gtk.MenuItem(_('File'))
293 file_menu = gtk.Menu()
294 file_menu.append(self.action_open.create_menu_item())
295 file_menu.append(self.action_save.create_menu_item())
296 file_menu.append(gtk.SeparatorMenuItem())
297 file_menu.append(self.action_quit.create_menu_item())
298 file_menu_item.set_submenu(file_menu)
299 menu_bar.append(file_menu_item)
301 tools_menu_item = gtk.MenuItem(_('Tools'))
302 tools_menu = gtk.Menu()
303 tools_menu.append(self.action_playlist.create_menu_item())
304 tools_menu_item.set_submenu(tools_menu)
305 menu_bar.append(tools_menu_item)
307 help_menu_item = gtk.MenuItem(_('Help'))
308 help_menu = gtk.Menu()
309 help_menu.append(self.action_about.create_menu_item())
310 help_menu_item.set_submenu(help_menu)
311 menu_bar.append(help_menu_item)
313 def create_menu(self):
314 # the main menu
315 menu = gtk.Menu()
317 menu_open = gtk.ImageMenuItem(_('Open playlist'))
318 menu_open.set_image(
319 gtk.image_new_from_stock(gtk.STOCK_OPEN, gtk.ICON_SIZE_MENU))
320 menu_open.connect("activate", self.open_file_callback)
321 menu.append(menu_open)
323 # the recent files menu
324 self.menu_recent = gtk.MenuItem(_('Open recent playlist'))
325 menu.append(self.menu_recent)
326 self.create_recent_files_menu()
328 menu.append(gtk.SeparatorMenuItem())
330 menu_save = gtk.ImageMenuItem(_('Save current playlist'))
331 menu_save.set_image(
332 gtk.image_new_from_stock(gtk.STOCK_SAVE_AS, gtk.ICON_SIZE_MENU))
333 menu_save.connect("activate", self.save_to_playlist_callback)
334 menu.append(menu_save)
336 menu.append(gtk.SeparatorMenuItem())
338 # the settings sub-menu
339 menu_settings = gtk.MenuItem(_('Settings'))
340 menu.append(menu_settings)
342 menu_settings_sub = gtk.Menu()
343 menu_settings.set_submenu(menu_settings_sub)
345 menu_settings_enable_dual_action = gtk.CheckMenuItem(
346 _('Enable dual-action buttons') )
347 settings.attach_checkbutton( menu_settings_enable_dual_action,
348 'enable_dual_action_btn' )
349 menu_settings_sub.append(menu_settings_enable_dual_action)
351 menu_settings_lock_progress = gtk.CheckMenuItem(_('Lock Progress Bar'))
352 settings.attach_checkbutton( menu_settings_lock_progress,
353 'progress_locked' )
354 menu_settings_sub.append(menu_settings_lock_progress)
356 menu_about = gtk.ImageMenuItem(gtk.STOCK_ABOUT)
357 menu_about.connect("activate", self.about_callback)
358 menu.append(menu_about)
360 menu.append(gtk.SeparatorMenuItem())
362 menu_quit = gtk.ImageMenuItem(gtk.STOCK_QUIT)
363 menu_quit.connect("activate", self.destroy)
364 menu.append(menu_quit)
366 return menu
368 def create_recent_files_menu( self ):
369 max_files = settings.max_recent_files
370 self.recent_files = player.playlist.get_recent_files(max_files)
371 menu_recent_sub = gtk.Menu()
373 if len(self.recent_files) > 0:
374 for f in self.recent_files:
375 # don't include the temporary playlist in the file list
376 if f == panucci.PLAYLIST_FILE: continue
377 # don't include non-existant files
378 if not os.path.exists( f ): continue
379 filename, extension = os.path.splitext(os.path.basename(f))
380 menu_item = gtk.MenuItem( filename.replace('_', ' '))
381 menu_item.connect('activate', self.on_recent_file_activate, f)
382 menu_recent_sub.append(menu_item)
383 else:
384 menu_item = gtk.MenuItem(_('No recent files available.'))
385 menu_item.set_sensitive(False)
386 menu_recent_sub.append(menu_item)
388 self.menu_recent.set_submenu(menu_recent_sub)
390 def notify(self, message):
391 """ Sends a notification using pynotify, returns message """
392 if platform.DESKTOP and have_pynotify:
393 icon = find_image('panucci_64x64.png')
394 notification = pynotify.Notification(self.main_window.get_title(), message, icon)
395 notification.show()
396 elif platform.MAEMO:
397 # Note: This won't work if we're not in the gtk main loop
398 markup = '<b>%s</b>\n<small>%s</small>' % (title, message)
399 hildon.hildon_banner_show_information_with_markup(self.main_window, None, markup)
401 def destroy(self, widget):
402 player.quit()
403 gtk.main_quit()
405 def show_main_window(self):
406 self.main_window.present()
408 def check_queue(self):
409 """ Makes sure the queue is saved if it has been modified
410 True means a new file can be opened
411 False means the user does not want to continue """
413 if not self.__ignore_queue_check and player.playlist.queue_modified:
414 response = dialog(
415 self.main_window, _('Save current playlist'),
416 _('Current playlist has been modified'),
417 _('Opening a new file will replace the current playlist. ') +
418 _('Do you want to save it before creating a new one?'),
419 affirmative_button=gtk.STOCK_SAVE,
420 negative_button=_('Discard changes'))
422 self.__log.debug('Response to "Save Queue?": %s', response)
424 if response is None:
425 return False
426 elif response:
427 return self.save_to_playlist_callback()
428 elif not response:
429 return True
430 else:
431 return False
432 else:
433 return True
435 def open_file_callback(self, widget=None):
436 if self.check_queue():
437 # set __ingnore__queue_check because we already did the check
438 self.__ignore_queue_check = True
439 filename = get_file_from_filechooser(self.main_window)
440 if filename is not None:
441 self._play_file(filename)
443 self.__ignore_queue_check = False
445 def save_to_playlist_callback(self, widget=None):
446 filename = get_file_from_filechooser(
447 self.main_window, save_file=True, save_to='playlist.m3u' )
449 if filename is None:
450 return False
452 if os.path.isfile(filename):
453 response = dialog( self.main_window, _('File already exists'),
454 _('File already exists'),
455 _('The file %s already exists. You can choose another name or '
456 'overwrite the existing file.') % os.path.basename(filename),
457 affirmative_button=gtk.STOCK_SAVE,
458 negative_button=_('Rename file'))
460 if response is None:
461 return None
463 elif response:
464 pass
465 elif not response:
466 return self.save_to_playlist_callback()
468 ext = util.detect_filetype(filename)
469 if not player.playlist.save_to_new_playlist(filename, ext):
470 self.notify(_('Error saving playlist...'))
471 return False
473 return True
475 def __get_fullscreen(self):
476 return self.__window_fullscreen
478 def __set_fullscreen(self, value):
479 if value != self.__window_fullscreen:
480 if value:
481 self.main_window.fullscreen()
482 else:
483 self.main_window.unfullscreen()
485 self.__window_fullscreen = value
486 player.playlist.send_metadata()
488 fullscreen = property( __get_fullscreen, __set_fullscreen )
490 def on_key_press(self, widget, event):
491 if platform.MAEMO:
492 if event.keyval == gtk.keysyms.F6:
493 self.fullscreen = not self.fullscreen
495 def on_recent_file_activate(self, widget, filepath):
496 self._play_file(filepath)
498 def on_file_queued(self, filepath, success, notify):
499 if notify:
500 filename = os.path.basename(filepath)
501 if success:
502 self.__log.info(
503 self.notify( '%s added successfully.' % filename ))
504 else:
505 self.__log.error(
506 self.notify( 'Error adding %s to the queue.' % filename))
508 def about_callback(self, widget):
509 dialog = gtk.AboutDialog()
510 dialog.set_website(about_website)
511 dialog.set_website_label(about_website)
512 dialog.set_name(about_name)
513 dialog.set_authors(about_authors)
514 dialog.set_comments(about_text)
515 dialog.set_version(panucci.__version__)
516 dialog.run()
517 dialog.destroy()
519 def _play_file(self, filename, pause_on_load=False):
520 player.playlist.load( os.path.abspath(filename) )
522 if player.playlist.is_empty:
523 return False
525 def handle_headset_button(self, event, button):
526 if event == 'ButtonPressed' and button == 'phone':
527 player.play_pause_toggle()
529 def __select_current_item( self ):
530 # Select the currently playing track in the playlist tab
531 # and switch to it (so we can edit bookmarks, etc.. there)
532 self.__playlist_tab.select_current_item()
533 self.playlist_window.show()
535 ##################################################
536 # PlayerTab
537 ##################################################
538 class PlayerTab(ObservableService, gtk.HBox):
539 """ The tab that holds the player elements """
541 signals = [ 'select-current-item-request', ]
543 def __init__(self, gui_root):
544 self.__log = logging.getLogger('panucci.panucci.PlayerTab')
545 self.__gui_root = gui_root
547 gtk.HBox.__init__(self)
548 ObservableService.__init__(self, self.signals, self.__log)
550 # Timers
551 self.progress_timer_id = None
552 self.volume_timer_id = None
554 self.recent_files = []
555 self.make_player_tab()
556 self.has_coverart = False
557 self.set_volume(settings.volume)
559 #settings.register( 'enable_dual_action_btn_changed',
560 # self.on_dual_action_setting_changed )
561 #settings.register( 'dual_action_button_delay_changed',
562 # self.on_dual_action_setting_changed )
563 #settings.register( 'volume_changed', self.set_volume )
564 #settings.register( 'scrolling_labels_changed', lambda v:
565 # setattr( self.title_label, 'scrolling', v ) )
567 player.register( 'stopped', self.on_player_stopped )
568 player.register( 'playing', self.on_player_playing )
569 player.register( 'paused', self.on_player_paused )
570 player.playlist.register( 'end-of-playlist',
571 self.on_player_end_of_playlist )
572 player.playlist.register( 'new-track-loaded',
573 self.on_player_new_track )
574 player.playlist.register( 'new-metadata-available',
575 self.on_player_new_metadata )
577 def make_player_tab(self):
578 main_vbox = gtk.VBox()
579 main_vbox.set_spacing(6)
580 # add a vbox to self
581 self.pack_start(main_vbox, True, True)
583 # a hbox to hold the cover art and metadata vbox
584 metadata_hbox = gtk.HBox()
585 metadata_hbox.set_spacing(6)
586 main_vbox.pack_start(metadata_hbox, True, False)
588 self.cover_art = gtk.Image()
589 metadata_hbox.pack_start( self.cover_art, False, False )
591 # vbox to hold metadata
592 metadata_vbox = gtk.VBox()
593 metadata_vbox.set_spacing(8)
594 empty_label = gtk.Label()
595 metadata_vbox.pack_start(empty_label, True, True)
596 self.artist_label = gtk.Label('')
597 self.artist_label.set_ellipsize(pango.ELLIPSIZE_END)
598 metadata_vbox.pack_start(self.artist_label, False, False)
599 self.album_label = gtk.Label('')
600 self.album_label.set_ellipsize(pango.ELLIPSIZE_END)
601 metadata_vbox.pack_start(self.album_label, False, False)
602 self.title_label = widgets.ScrollingLabel( '',
603 update_interval=200,
604 pixel_jump=5,
605 delay_btwn_scrolls=5000,
606 delay_halfway=3000 )
607 self.title_label.scrolling = settings.scrolling_labels
608 metadata_vbox.pack_start(self.title_label, False, False)
609 empty_label = gtk.Label()
610 metadata_vbox.pack_start(empty_label, True, True)
611 metadata_hbox.pack_start( metadata_vbox, True, True )
613 progress_eventbox = gtk.EventBox()
614 progress_eventbox.set_events(gtk.gdk.BUTTON_PRESS_MASK)
615 progress_eventbox.connect(
616 'button-press-event', self.on_progressbar_changed )
617 self.progress = gtk.ProgressBar()
618 # make the progress bar more "finger-friendly"
619 if platform.FREMANTLE:
620 self.progress.set_size_request(-1, 100)
621 elif platform.MAEMO:
622 self.progress.set_size_request(-1, 50)
623 progress_eventbox.add(self.progress)
624 main_vbox.pack_start( progress_eventbox, False, False )
626 # make the button box
627 buttonbox = gtk.HBox()
629 # A wrapper to help create DualActionButtons with the right settings
630 create_da = lambda a, b, c=None, d=None: widgets.DualActionButton(
631 a, b, c, d, settings.dual_action_button_delay,
632 settings.enable_dual_action_btn )
634 self.rrewind_button = create_da(
635 generate_image('media-skip-backward.png'),
636 lambda: self.do_seek(-1*settings.seek_long),
637 generate_image(gtk.STOCK_GOTO_FIRST, True),
638 player.playlist.prev)
639 buttonbox.add(self.rrewind_button)
641 self.rewind_button = create_da(
642 generate_image('media-seek-backward.png'),
643 lambda: self.do_seek(-1*settings.seek_short))
644 buttonbox.add(self.rewind_button)
646 self.play_pause_button = gtk.Button('')
647 image(self.play_pause_button, 'media-playback-start.png')
648 self.play_pause_button.connect( 'clicked',
649 self.on_btn_play_pause_clicked )
650 self.play_pause_button.set_sensitive(False)
651 buttonbox.add(self.play_pause_button)
653 self.forward_button = create_da(
654 generate_image('media-seek-forward.png'),
655 lambda: self.do_seek(settings.seek_short))
656 buttonbox.add(self.forward_button)
658 self.fforward_button = create_da(
659 generate_image('media-skip-forward.png'),
660 lambda: self.do_seek(settings.seek_long),
661 generate_image(gtk.STOCK_GOTO_LAST, True),
662 player.playlist.next)
663 buttonbox.add(self.fforward_button)
665 self.bookmarks_button = create_da(
666 generate_image('bookmark-new.png'),
667 player.add_bookmark_at_current_position,
668 generate_image(gtk.STOCK_JUMP_TO, True),
669 lambda *args: self.notify('select-current-item-request'))
670 buttonbox.add(self.bookmarks_button)
671 self.set_controls_sensitivity(False)
672 main_vbox.pack_start(buttonbox, False, False)
674 if platform.MAEMO:
675 self.volume = hildon.VVolumebar()
676 self.volume.set_property('can-focus', False)
677 self.volume.connect('level_changed', self.volume_changed_hildon)
678 self.volume.connect('mute_toggled', self.mute_toggled)
679 self.__gui_root.main_window.connect( 'key-press-event',
680 self.on_key_press )
681 if not platform.FREMANTLE:
682 self.pack_start(self.volume, False, True)
684 # Add a button to pop out the volume bar
685 self.volume_button = gtk.ToggleButton('')
686 image(self.volume_button, 'media-speaker.png')
687 self.volume_button.connect('clicked', self.toggle_volumebar)
688 self.volume.connect(
689 'show', lambda x: self.volume_button.set_active(True))
690 self.volume.connect(
691 'hide', lambda x: self.volume_button.set_active(False))
692 if not platform.FREMANTLE:
693 buttonbox.add(self.volume_button)
694 self.volume_button.show()
696 # Disable focus for all widgets, so we can use the cursor
697 # keys + enter to directly control our media player, which
698 # is handled by "key-press-event"
699 for w in (
700 self.rrewind_button, self.rewind_button,
701 self.play_pause_button, self.forward_button,
702 self.fforward_button, self.progress,
703 self.bookmarks_button, self.volume_button, ):
704 w.unset_flags(gtk.CAN_FOCUS)
705 else:
706 self.volume = gtk.VolumeButton()
707 self.volume.connect('value-changed', self.volume_changed_gtk)
708 buttonbox.add(self.volume)
709 self.volume.show()
711 self.set_volume(settings.volume)
713 def set_controls_sensitivity(self, sensitive):
714 for button in self.forward_button, self.rewind_button, \
715 self.fforward_button, self.rrewind_button:
717 button.set_sensitive(sensitive)
719 # the play/pause button should always be available except
720 # for when the player starts without a file
721 self.play_pause_button.set_sensitive(True)
723 def on_dual_action_setting_changed( self, *args ):
724 for button in self.forward_button, self.rewind_button, \
725 self.fforward_button, self.rrewind_button, \
726 self.bookmarks_button:
728 button.set_longpress_enabled( settings.enable_dual_action_btn )
729 button.set_duration( settings.dual_action_button_delay )
731 def on_key_press(self, widget, event):
732 if platform.MAEMO:
733 if event.keyval == gtk.keysyms.F7: #plus
734 self.set_volume( min( 1, self.get_volume() + 0.10 ))
735 elif event.keyval == gtk.keysyms.F8: #minus
736 self.set_volume( max( 0, self.get_volume() - 0.10 ))
737 elif event.keyval == gtk.keysyms.Left: # seek back
738 self.do_seek( -1 * settings.seek_long )
739 elif event.keyval == gtk.keysyms.Right: # seek forward
740 self.do_seek( settings.seek_long )
741 elif event.keyval == gtk.keysyms.Return: # play/pause
742 self.on_btn_play_pause_clicked()
744 # The following two functions get and set the
745 # volume from the volume control widgets.
746 def get_volume(self):
747 if platform.MAEMO:
748 return self.volume.get_level()/100.0
749 else:
750 return self.volume.get_value()
752 def set_volume(self, vol):
753 """ vol is a float from 0 to 1 """
754 assert 0 <= vol <= 1
756 if platform.FREMANTLE:
757 # No volume setting on Maemo 5
758 return
760 if platform.MAEMO:
761 self.volume.set_level(vol*100.0)
762 else:
763 self.volume.set_value(vol)
765 def __set_volume_hide_timer(self, timeout, force_show=False):
766 if force_show or self.volume_button.get_active():
767 self.volume.show()
768 if self.volume_timer_id is not None:
769 gobject.source_remove(self.volume_timer_id)
770 self.volume_timer_id = None
772 self.volume_timer_id = gobject.timeout_add(
773 1000 * timeout, self.__volume_hide_callback )
775 def __volume_hide_callback(self):
776 self.volume_timer_id = None
777 self.volume.hide()
778 return False
780 def toggle_volumebar(self, widget=None):
781 if self.volume_timer_id is None:
782 self.__set_volume_hide_timer(5)
783 else:
784 self.__volume_hide_callback()
786 def volume_changed_gtk(self, widget, new_value=0.5):
787 settings.volume = new_value
789 def volume_changed_hildon(self, widget):
790 self.__set_volume_hide_timer( 4, force_show=True )
791 settings.volume = widget.get_level()/100.0
793 def mute_toggled(self, widget):
794 if widget.get_mute():
795 settings.volume = 0
796 else:
797 settings.volume = widget.get_level()/100.0
799 def on_player_stopped(self):
800 self.stop_progress_timer()
801 self.set_controls_sensitivity(False)
802 image(self.play_pause_button, 'media-playback-start.png')
804 def on_player_playing(self):
805 self.start_progress_timer()
806 image(self.play_pause_button, 'media-playback-pause.png')
807 self.set_controls_sensitivity(True)
809 def on_player_new_track(self):
810 for widget in [self.title_label,self.artist_label,self.album_label]:
811 widget.set_markup('')
812 widget.hide()
814 self.cover_art.hide()
815 self.has_coverart = False
817 def on_player_new_metadata(self):
818 metadata = player.playlist.get_file_metadata()
819 self.set_metadata(metadata)
821 if not player.playing:
822 position = player.playlist.get_current_position()
823 estimated_length = metadata.get('length', 0)
824 self.set_progress_callback( position, estimated_length )
826 def on_player_paused( self, position, duration ):
827 self.stop_progress_timer() # This should save some power
828 self.set_progress_callback( position, duration )
829 image(self.play_pause_button, 'media-playback-start.png')
831 def on_player_end_of_playlist(self, loop):
832 pass
834 def reset_progress(self):
835 self.progress.set_fraction(0)
836 self.set_progress_callback(0,0)
838 def set_progress_callback(self, time_elapsed, total_time):
839 """ times must be in nanoseconds """
840 time_string = "%s / %s" % ( util.convert_ns(time_elapsed),
841 util.convert_ns(total_time) )
842 self.progress.set_text( time_string )
843 fraction = float(time_elapsed) / float(total_time) if total_time else 0
844 self.progress.set_fraction( fraction )
846 def on_progressbar_changed(self, widget, event):
847 if ( not settings.progress_locked and
848 event.type == gtk.gdk.BUTTON_PRESS and event.button == 1 ):
849 new_fraction = event.x/float(widget.get_allocation().width)
850 resp = player.do_seek(percent=new_fraction)
851 if resp:
852 # Preemptively update the progressbar to make seeking smoother
853 self.set_progress_callback( *resp )
855 def on_btn_play_pause_clicked(self, widget=None):
856 player.play_pause_toggle()
858 def progress_timer_callback( self ):
859 if player.playing and not player.seeking:
860 pos_int, dur_int = player.get_position_duration()
861 # This prevents bogus values from being set while seeking
862 if ( pos_int > 10**9 ) and ( dur_int > 10**9 ):
863 self.set_progress_callback( pos_int, dur_int )
864 return True
866 def start_progress_timer( self ):
867 if self.progress_timer_id is not None:
868 self.stop_progress_timer()
870 self.progress_timer_id = gobject.timeout_add(
871 1000, self.progress_timer_callback )
873 def stop_progress_timer( self ):
874 if self.progress_timer_id is not None:
875 gobject.source_remove( self.progress_timer_id )
876 self.progress_timer_id = None
878 def get_coverart_size( self ):
879 if platform.MAEMO:
880 if self.__gui_root.fullscreen:
881 size = coverart_sizes['maemo fullscreen']
882 else:
883 size = coverart_sizes['maemo']
884 else:
885 size = coverart_sizes['normal']
887 return size, size
889 def set_coverart( self, pixbuf ):
890 self.cover_art.set_from_pixbuf(pixbuf)
891 self.cover_art.show()
892 self.has_coverart = True
894 def set_metadata( self, tag_message ):
895 tags = { 'title': self.title_label, 'artist': self.artist_label,
896 'album': self.album_label }
898 # set the coverart
899 if tag_message.has_key('image') and tag_message['image'] is not None:
900 value = tag_message['image']
902 pbl = gtk.gdk.PixbufLoader()
903 try:
904 pbl.write(value)
905 pbl.close()
907 x, y = self.get_coverart_size()
908 pixbuf = pbl.get_pixbuf()
909 pixbuf = pixbuf.scale_simple( x, y, gtk.gdk.INTERP_BILINEAR )
910 self.set_coverart(pixbuf)
911 except Exception, e:
912 self.__log.exception('Error setting coverart...')
914 # set the text metadata
915 for tag,value in tag_message.iteritems():
916 if tags.has_key(tag) and value is not None and value.strip():
917 tags[tag].set_markup('<big>'+value+'</big>')
918 tags[tag].set_alignment( 0.5*int(not self.has_coverart), 0.5)
919 tags[tag].show()
921 if tag == 'title':
922 # make the title bold
923 tags[tag].set_markup('<b><big>'+value+'</big></b>')
925 if not platform.MAEMO:
926 value += ' - Panucci'
928 self.__gui_root.main_window.set_title( value )
930 def do_seek(self, seek_amount):
931 resp = player.do_seek(from_current=seek_amount*10**9)
932 if resp:
933 # Preemptively update the progressbar to make seeking smoother
934 self.set_progress_callback( *resp )
938 ##################################################
939 # PlaylistTab
940 ##################################################
941 class PlaylistTab(gtk.VBox):
942 def __init__(self, main_window):
943 gtk.VBox.__init__(self)
944 self.__log = logging.getLogger('panucci.panucci.BookmarksWindow')
945 self.main = main_window
947 self.__model = gtk.TreeStore(
948 # uid, name, position
949 gobject.TYPE_STRING, gobject.TYPE_STRING, gobject.TYPE_STRING )
951 self.set_spacing(5)
952 self.treeview = gtk.TreeView()
953 self.treeview.set_model(self.__model)
954 self.treeview.set_headers_visible(True)
955 tree_selection = self.treeview.get_selection()
956 # This breaks drag and drop, only use single selection for now
957 # tree_selection.set_mode(gtk.SELECTION_MULTIPLE)
958 tree_selection.connect('changed', self.tree_selection_changed)
960 # The tree lines look nasty on maemo
961 if platform.DESKTOP:
962 self.treeview.set_enable_tree_lines(True)
963 self.update_model()
965 ncol = gtk.TreeViewColumn(_('Name'))
966 ncell = gtk.CellRendererText()
967 ncell.set_property('ellipsize', pango.ELLIPSIZE_END)
968 ncell.set_property('editable', True)
969 ncell.connect('edited', self.label_edited)
970 ncol.set_expand(True)
971 ncol.pack_start(ncell)
972 ncol.add_attribute(ncell, 'text', 1)
974 tcol = gtk.TreeViewColumn(_('Position'))
975 tcell = gtk.CellRendererText()
976 tcol.pack_start(tcell)
977 tcol.add_attribute(tcell, 'text', 2)
979 self.treeview.append_column(ncol)
980 self.treeview.append_column(tcol)
981 self.treeview.connect('drag-data-received', self.drag_data_recieved)
982 self.treeview.connect('drag_data_get', self.drag_data_get_data)
984 treeview_targets = [
985 ( 'playlist_row_data', gtk.TARGET_SAME_WIDGET, 0 ) ]
987 self.treeview.enable_model_drag_source(
988 gtk.gdk.BUTTON1_MASK, treeview_targets, gtk.gdk.ACTION_COPY )
990 self.treeview.enable_model_drag_dest(
991 treeview_targets, gtk.gdk.ACTION_COPY )
993 sw = gtk.ScrolledWindow()
994 sw.set_policy(gtk.POLICY_AUTOMATIC, gtk.POLICY_AUTOMATIC)
995 sw.set_shadow_type(gtk.SHADOW_IN)
996 sw.add(self.treeview)
997 self.add(sw)
999 self.hbox = gtk.HBox()
1001 self.add_button = gtk.Button(gtk.STOCK_NEW)
1002 self.add_button.set_use_stock(True)
1003 set_stock_button_text( self.add_button, _('Add File') )
1004 self.add_button.connect('clicked', self.add_file)
1005 self.hbox.pack_start(self.add_button, True, True)
1007 self.dir_button = gtk.Button(gtk.STOCK_OPEN)
1008 self.dir_button.set_use_stock(True)
1009 set_stock_button_text( self.dir_button, _('Add Directory') )
1010 self.dir_button.connect('clicked', self.add_directory)
1011 self.hbox.pack_start(self.dir_button, True, True)
1013 self.remove_button = widgets.DualActionButton(
1014 generate_image(gtk.STOCK_REMOVE, True),
1015 self.remove_bookmark,
1016 generate_image(gtk.STOCK_CANCEL, True),
1017 lambda *a: player.playlist.reset_playlist() )
1018 #self.remove_button.set_use_stock(True)
1019 #self.remove_button.connect('clicked', self.remove_bookmark)
1020 self.hbox.pack_start(self.remove_button, True, True)
1022 self.jump_button = gtk.Button(gtk.STOCK_JUMP_TO)
1023 self.jump_button.set_use_stock(True)
1024 self.jump_button.connect('clicked', self.jump_bookmark)
1025 self.hbox.pack_start(self.jump_button, True, True)
1027 self.info_button = gtk.Button()
1028 self.info_button.add(
1029 gtk.image_new_from_stock(gtk.STOCK_INFO, gtk.ICON_SIZE_BUTTON))
1030 self.info_button.connect('clicked', self.show_playlist_item_details)
1031 self.hbox.pack_start(self.info_button, True, True)
1033 self.pack_start(self.hbox, False, True)
1035 player.playlist.register( 'file_queued',
1036 lambda x,y,z: self.update_model() )
1037 player.playlist.register( 'bookmark_added', self.on_bookmark_added )
1039 self.show_all()
1041 def tree_selection_changed(self, treeselection):
1042 count = treeselection.count_selected_rows()
1043 self.remove_button.set_sensitive(count > 0)
1044 self.jump_button.set_sensitive(count == 1)
1045 self.info_button.set_sensitive(count == 1)
1047 def drag_data_get_data(
1048 self, treeview, context, selection, target_id, timestamp):
1050 treeselection = treeview.get_selection()
1051 model, iter = treeselection.get_selected()
1052 # only allow moving around top-level parents
1053 if model.iter_parent(iter) is None:
1054 # send the path of the selected row
1055 data = model.get_string_from_iter(iter)
1056 selection.set(selection.target, 8, data)
1057 else:
1058 self.__log.debug("Can't move children...")
1060 def drag_data_recieved(
1061 self, treeview, context, x, y, selection, info, timestamp):
1063 drop_info = treeview.get_dest_row_at_pos(x, y)
1065 # TODO: If user drags the row past the last row, drop_info is None
1066 # I'm not sure if it's safe to simply assume that None is
1067 # euqivalent to the last row...
1068 if None not in [ drop_info and selection.data ]:
1069 model = treeview.get_model()
1070 path, position = drop_info
1072 from_iter = model.get_iter_from_string(selection.data)
1074 # make sure the to_iter doesn't have a parent
1075 to_iter = model.get_iter(path)
1076 if model.iter_parent(to_iter) is not None:
1077 to_iter = model.iter_parent(to_iter)
1079 from_row = model.get_path(from_iter)[0]
1080 to_row = path[0]
1082 if ( position == gtk.TREE_VIEW_DROP_BEFORE or
1083 position == gtk.TREE_VIEW_DROP_INTO_OR_BEFORE ):
1084 model.move_before( from_iter, to_iter )
1085 to_row = to_row - 1 if from_row < to_row else to_row
1086 elif ( position == gtk.TREE_VIEW_DROP_AFTER or
1087 position == gtk.TREE_VIEW_DROP_INTO_OR_AFTER ):
1088 model.move_after( from_iter, to_iter )
1089 to_row = to_row + 1 if from_row > to_row else to_row
1090 else:
1091 self.__log.debug('Drop not supported: %s', position)
1093 # don't do anything if we're not actually moving rows around
1094 if from_row != to_row:
1095 player.playlist.move_item( from_row, to_row )
1097 else:
1098 self.__log.debug('No drop_data or selection.data available')
1100 def update_model(self):
1101 plist = player.playlist
1102 path_info = self.treeview.get_path_at_pos(0,0)
1103 path = path_info[0] if path_info is not None else None
1105 self.__model.clear()
1107 # build the tree
1108 for item, data in plist.get_playlist_item_ids():
1109 parent = self.__model.append(None, (item, data.get('title'), None))
1111 for bid, bname, bpos in plist.get_bookmarks_from_item_id( item ):
1112 nice_bpos = util.convert_ns(bpos)
1113 self.__model.append( parent, (bid, bname, nice_bpos) )
1115 self.treeview.expand_all()
1117 if path is not None:
1118 self.treeview.scroll_to_cell(path)
1120 def label_edited(self, cellrenderer, path, new_text):
1121 iter = self.__model.get_iter(path)
1122 old_text = self.__model.get_value(iter, 1)
1124 if new_text.strip() and old_text != new_text:
1125 # this loop will only run once, because only one cell can be
1126 # edited at a time, we use it to get the item and bookmark ids
1127 for m, bkmk_id, biter, item_id, iiter in self.__cur_selection():
1128 self.__model.set_value(iter, 1, new_text)
1129 player.playlist.update_bookmark(
1130 item_id, bkmk_id, name=new_text )
1131 else:
1132 self.__model.set_value(iter, 1, old_text)
1134 def on_bookmark_added(self, parent_id, bookmark_name, position):
1135 self.main.notify(_('Bookmark added: %s') % bookmark_name)
1136 self.update_model()
1138 def add_file(self, widget):
1139 filename = get_file_from_filechooser(self.main.main_window)
1140 if filename is not None:
1141 player.playlist.append(filename)
1143 def add_directory(self, widget):
1144 directory = get_file_from_filechooser(
1145 self.main.main_window, folder=True )
1146 if directory is not None:
1147 player.playlist.load_directory(directory, append=True)
1149 def __cur_selection(self):
1150 selection = self.treeview.get_selection()
1151 model, bookmark_paths = selection.get_selected_rows()
1153 # Convert the paths to gtk.TreeRowReference objects, because we
1154 # might modify the model while this generator is running
1155 bookmark_refs = [gtk.TreeRowReference(model, p) for p in bookmark_paths]
1157 for reference in bookmark_refs:
1158 bookmark_iter = model.get_iter(reference.get_path())
1159 item_iter = model.iter_parent(bookmark_iter)
1161 # bookmark_iter is actually an item_iter
1162 if item_iter is None:
1163 item_iter = bookmark_iter
1164 item_id = model.get_value(item_iter, 0)
1165 bookmark_id, bookmark_iter = None, None
1166 else:
1167 bookmark_id = model.get_value(bookmark_iter, 0)
1168 item_id = model.get_value(item_iter, 0)
1170 yield model, bookmark_id, bookmark_iter, item_id, item_iter
1172 def remove_bookmark(self, w=None):
1173 for model, bkmk_id, bkmk_iter, item_id, item_iter in self.__cur_selection():
1174 player.playlist.remove_bookmark( item_id, bkmk_id )
1175 if bkmk_iter is not None:
1176 model.remove(bkmk_iter)
1177 elif item_iter is not None:
1178 model.remove(item_iter)
1180 def select_current_item(self):
1181 model = self.treeview.get_model()
1182 selection = self.treeview.get_selection()
1183 current_item_id = str(player.playlist.get_current_item())
1184 for row in iter(model):
1185 if model.get_value(row.iter, 0) == current_item_id:
1186 selection.unselect_all()
1187 self.treeview.set_cursor(row.path)
1188 self.treeview.scroll_to_cell(row.path, use_align=True)
1189 break
1191 def show_playlist_item_details(self, w):
1192 selection = self.treeview.get_selection()
1193 if selection.count_selected_rows() == 1:
1194 selected = self.__cur_selection().next()
1195 model, bkmk_id, bkmk_iter, item_id, item_iter = selected
1196 playlist_item = player.playlist.get_item_by_id(item_id)
1197 PlaylistItemDetails(self.main, playlist_item)
1199 def jump_bookmark(self, w):
1200 selected = list(self.__cur_selection())
1201 if len(selected) == 1:
1202 # It should be guranteed by the fact that we only enable the
1203 # "Jump to" button when the selection count equals 1.
1204 model, bkmk_id, bkmk_iter, item_id, item_iter = selected.pop(0)
1205 player.playlist.load_from_bookmark_id(item_id, bkmk_id)
1207 # FIXME: The player/playlist should be able to take care of this
1208 if not player.playing:
1209 player.play()
1212 ##################################################
1213 # PlaylistItemDetails
1214 ##################################################
1215 class PlaylistItemDetails(gtk.Dialog):
1216 def __init__(self, main, playlist_item):
1217 gtk.Dialog.__init__( self, _('Playlist item details'),
1218 main.main_window, gtk.DIALOG_MODAL,
1219 (gtk.STOCK_CLOSE, gtk.RESPONSE_OK))
1221 self.main = main
1222 self.fill(playlist_item)
1223 self.set_has_separator(False)
1224 self.set_resizable(False)
1225 self.show_all()
1226 self.run()
1227 self.destroy()
1229 def fill(self, playlist_item):
1230 t = gtk.Table(10, 2)
1231 self.vbox.pack_start(t, expand=False)
1233 metadata = playlist_item.metadata
1235 t.attach(gtk.Label(_('Custom title:')), 0, 1, 0, 1)
1236 t.attach(gtk.Label(_('ID:')), 0, 1, 1, 2)
1237 t.attach(gtk.Label(_('Playlist ID:')), 0, 1, 2, 3)
1238 t.attach(gtk.Label(_('Filepath:')), 0, 1, 3, 4)
1240 row_num = 4
1241 for key in metadata:
1242 if metadata[key] is not None:
1243 t.attach( gtk.Label(key.capitalize()+':'),
1244 0, 1, row_num, row_num+1 )
1245 row_num += 1
1247 t.foreach(lambda x, y: x.set_alignment(1, 0.5), None)
1248 t.foreach(lambda x, y: x.set_markup('<b>%s</b>' % x.get_label()), None)
1250 t.attach(gtk.Label(playlist_item.title or _('<not modified>')),1,2,0,1)
1251 t.attach(gtk.Label(str(playlist_item)), 1, 2, 1, 2)
1252 t.attach(gtk.Label(playlist_item.playlist_id), 1, 2, 2, 3)
1253 t.attach(gtk.Label(playlist_item.filepath), 1, 2, 3, 4)
1255 row_num = 4
1256 for key in metadata:
1257 value = metadata[key]
1258 if key == 'length':
1259 value = util.convert_ns(value)
1260 if metadata[key] is not None:
1261 t.attach( gtk.Label( str(value) or _('<not set>')),
1262 1, 2, row_num, row_num+1)
1263 row_num += 1
1265 t.foreach(lambda x, y: x.get_alignment() == (0.5, 0.5) and \
1266 x.set_alignment(0, 0.5), None)
1268 t.set_border_width(8)
1269 t.set_row_spacings(4)
1270 t.set_col_spacings(8)
1272 l = gtk.ListStore(str, str)
1273 t = gtk.TreeView(l)
1274 cr = gtk.CellRendererText()
1275 cr.set_property('ellipsize', pango.ELLIPSIZE_END)
1276 c = gtk.TreeViewColumn(_('Title'), cr, text=0)
1277 c.set_expand(True)
1278 t.append_column(c)
1279 c = gtk.TreeViewColumn(_('Time'), gtk.CellRendererText(), text=1)
1280 t.append_column(c)
1281 playlist_item.load_bookmarks()
1282 for bookmark in playlist_item.bookmarks:
1283 l.append([bookmark.bookmark_name, \
1284 util.convert_ns(bookmark.seek_position)])
1286 sw = gtk.ScrolledWindow()
1287 sw.set_shadow_type(gtk.SHADOW_IN)
1288 sw.add(t)
1289 sw.set_policy(gtk.POLICY_AUTOMATIC, gtk.POLICY_AUTOMATIC)
1290 e = gtk.Expander(_('Bookmarks'))
1291 e.add(sw)
1292 self.vbox.pack_start(e)
1295 def run(filename=None):
1296 PanucciGUI( filename )
1297 gtk.main()
1299 if __name__ == '__main__':
1300 log.error( 'Use the "panucci" executable to run this program.' )
1301 log.error( 'Exiting...' )
1302 sys.exit(1)