Clean up platform detection code
[panucci.git] / src / panucci / main.py
blob6eca57ba76dce51009ad37cdc6b2c9bb19e431bb
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
42 log = logging.getLogger('panucci.panucci')
45 try:
46 import hildon
47 except:
48 if util.platform.MAEMO:
49 log.critical( 'Using GTK widgets, install "python2.5-hildon" '
50 'for this to work properly.' )
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/'
62 coverart_sizes = {
63 'normal' : 110,
64 'maemo' : 200,
65 'maemo fullscreen' : 275,
68 gtk.about_dialog_set_url_hook(util.open_link, None)
69 gtk.icon_size_register('panucci-button', 32, 32)
71 def generate_image(filename, is_stock=False):
72 image = None
73 if is_stock:
74 image = gtk.image_new_from_stock(
75 filename, gtk.icon_size_from_name('panucci-button') )
76 else:
77 filename = util.find_image(filename)
78 if filename is not None:
79 image = gtk.image_new_from_file(filename)
80 if image is not None:
81 if util.platform.MAEMO:
82 image.set_padding(20, 20)
83 else:
84 image.set_padding(5, 5)
85 image.show()
86 return image
88 def image(widget, filename, is_stock=False):
89 child = widget.get_child()
90 if child is not None:
91 widget.remove(child)
92 image = generate_image(filename, is_stock)
93 if image is not None:
94 widget.add(image)
96 def dialog( toplevel_window, title, question, description,
97 affirmative_button=gtk.STOCK_YES, negative_button=gtk.STOCK_NO,
98 abortion_button=gtk.STOCK_CANCEL ):
100 """Present the user with a yes/no/cancel dialog.
101 The return value is either True, False or None, depending on which
102 button has been pressed in the dialog:
104 affirmative button (default: Yes) => True
105 negative button (defaut: No) => False
106 abortion button (default: Cancel) => None
108 When the dialog is closed with the "X" button in the window manager
109 decoration, the return value is always None (same as abortion button).
111 You can set any of the affirmative_button, negative_button or
112 abortion_button values to "None" to hide the corresponding action.
114 dlg = gtk.MessageDialog( toplevel_window, gtk.DIALOG_MODAL,
115 gtk.MESSAGE_QUESTION, message_format=question )
117 dlg.set_title(title)
119 if abortion_button is not None:
120 dlg.add_button(abortion_button, gtk.RESPONSE_CANCEL)
121 if negative_button is not None:
122 dlg.add_button(negative_button, gtk.RESPONSE_NO)
123 if affirmative_button is not None:
124 dlg.add_button(affirmative_button, gtk.RESPONSE_YES)
126 dlg.format_secondary_text(description)
128 response = dlg.run()
129 dlg.destroy()
131 if response == gtk.RESPONSE_YES:
132 return True
133 elif response == gtk.RESPONSE_NO:
134 return False
135 elif response in [gtk.RESPONSE_CANCEL, gtk.RESPONSE_DELETE_EVENT]:
136 return None
138 def get_file_from_filechooser(
139 toplevel_window, folder=False, save_file=False, save_to=None):
141 if folder:
142 open_action = gtk.FILE_CHOOSER_ACTION_SELECT_FOLDER
143 else:
144 open_action = gtk.FILE_CHOOSER_ACTION_OPEN
146 if util.platform.MAEMO:
147 if save_file:
148 args = ( toplevel_window, gtk.FILE_CHOOSER_ACTION_SAVE )
149 else:
150 args = ( toplevel_window, open_action )
152 dlg = hildon.FileChooserDialog( *args )
153 else:
154 if save_file:
155 args = ( _('Select file to save playlist to'), None,
156 gtk.FILE_CHOOSER_ACTION_SAVE,
157 (( gtk.STOCK_CANCEL, gtk.RESPONSE_REJECT,
158 gtk.STOCK_SAVE, gtk.RESPONSE_OK )) )
159 else:
160 args = ( _('Select podcast or audiobook'), None, open_action,
161 (( gtk.STOCK_CANCEL, gtk.RESPONSE_REJECT,
162 gtk.STOCK_OPEN, gtk.RESPONSE_OK )) )
164 dlg = gtk.FileChooserDialog(*args)
166 current_folder = os.path.expanduser(settings.last_folder)
168 if current_folder is not None and os.path.isdir(current_folder):
169 dlg.set_current_folder(current_folder)
171 if save_file and save_to is not None:
172 dlg.set_current_name(save_to)
174 if dlg.run() == gtk.RESPONSE_OK:
175 filename = dlg.get_filename()
176 settings.last_folder = dlg.get_current_folder()
177 else:
178 filename = None
180 dlg.destroy()
181 return filename
183 def set_stock_button_text( button, text ):
184 alignment = button.get_child()
185 hbox = alignment.get_child()
186 image, label = hbox.get_children()
187 label.set_text(text)
189 ##################################################
190 # PanucciGUI
191 ##################################################
192 class PanucciGUI(object):
193 """ The object that holds the entire panucci gui """
195 def __init__(self, filename=None):
196 self.__log = logging.getLogger('panucci.panucci.PanucciGUI')
197 interface.register_gui(self)
199 # Build the base ui (window and menubar)
200 if util.platform.MAEMO:
201 self.app = hildon.Program()
202 window = hildon.Window()
203 self.app.add_window(window)
204 else:
205 window = gtk.Window(gtk.WINDOW_TOPLEVEL)
207 self.main_window = window
208 window.set_title('Panucci')
209 self.window_icon = util.find_image('panucci.png')
210 if self.window_icon is not None:
211 window.set_icon_from_file( self.window_icon )
212 window.set_default_size(400, -1)
213 window.set_border_width(0)
214 window.connect("destroy", self.destroy)
216 # Add the tabs (they are private to prevent us from trying to do
217 # something like gui_root.player_tab.some_function() from inside
218 # playlist_tab or vice-versa)
219 self.__player_tab = PlayerTab(self)
220 self.__playlist_tab = PlaylistTab(self)
222 self.playlist_window = gtk.Window(gtk.WINDOW_TOPLEVEL)
223 self.playlist_window.connect('delete-event', gtk.Widget.hide_on_delete)
224 self.playlist_window.set_title(_('Panucci playlist'))
225 self.playlist_window.set_transient_for(self.main_window)
226 self.playlist_window.add(self.__playlist_tab)
228 self.create_actions()
230 if util.platform.MAEMO:
231 window.set_menu(self.create_menu())
232 window.add(self.__player_tab)
233 else:
234 menu_vbox = gtk.VBox()
235 menu_vbox.set_spacing(0)
236 window.add(menu_vbox)
237 menu_bar = gtk.MenuBar()
238 self.create_desktop_menu(menu_bar)
239 menu_vbox.pack_start(menu_bar, False, False, 0)
240 menu_bar.show()
241 menu_vbox.pack_end(self.__player_tab, True, True, 6)
243 # Tie it all together!
244 self.__ignore_queue_check = False
245 self.__window_fullscreen = False
247 if util.platform.MAEMO and interface.headset_device is not None:
248 # Enable play/pause with headset button
249 interface.headset_device.connect_to_signal(
250 'Condition', self.handle_headset_button )
252 self.main_window.connect('key-press-event', self.on_key_press)
253 player.playlist.register( 'file_queued', self.on_file_queued )
255 player.playlist.register( 'playlist-to-be-overwritten',
256 self.check_queue )
257 self.__player_tab.register( 'select-current-item-request',
258 self.__select_current_item )
260 self.main_window.show_all()
262 # this should be done when the gui is ready
263 player.init(filepath=filename)
265 def create_actions(self):
266 self.action_open = gtk.Action('open', _('Open'), _('Open a file or playlist'), gtk.STOCK_OPEN)
267 self.action_open.connect('activate', self.open_file_callback)
268 self.action_save = gtk.Action('save', _('Save playlist'), _('Save current playlist to file'), gtk.STOCK_SAVE_AS)
269 self.action_save.connect('activate', self.save_to_playlist_callback)
270 self.action_playlist = gtk.Action('playlist', _('Playlist'), _('Open the current playlist'), None)
271 self.action_playlist.connect('activate', lambda a: self.playlist_window.show())
272 self.action_about = gtk.Action('about', _('About Panucci'), _('Show application version'), gtk.STOCK_ABOUT)
273 self.action_about.connect('activate', self.about_callback)
274 self.action_quit = gtk.Action('quit', _('Quit'), _('Close Panucci'), gtk.STOCK_QUIT)
275 self.action_quit.connect('activate', self.destroy)
277 def create_desktop_menu(self, menu_bar):
278 file_menu_item = gtk.MenuItem(_('File'))
279 file_menu = gtk.Menu()
280 file_menu.append(self.action_open.create_menu_item())
281 file_menu.append(self.action_save.create_menu_item())
282 file_menu.append(gtk.SeparatorMenuItem())
283 file_menu.append(self.action_quit.create_menu_item())
284 file_menu_item.set_submenu(file_menu)
285 menu_bar.append(file_menu_item)
287 tools_menu_item = gtk.MenuItem(_('Tools'))
288 tools_menu = gtk.Menu()
289 tools_menu.append(self.action_playlist.create_menu_item())
290 tools_menu_item.set_submenu(tools_menu)
291 menu_bar.append(tools_menu_item)
293 help_menu_item = gtk.MenuItem(_('Help'))
294 help_menu = gtk.Menu()
295 help_menu.append(self.action_about.create_menu_item())
296 help_menu_item.set_submenu(help_menu)
297 menu_bar.append(help_menu_item)
299 def create_menu(self):
300 # the main menu
301 menu = gtk.Menu()
303 menu_open = gtk.ImageMenuItem(_('Open playlist'))
304 menu_open.set_image(
305 gtk.image_new_from_stock(gtk.STOCK_OPEN, gtk.ICON_SIZE_MENU))
306 menu_open.connect("activate", self.open_file_callback)
307 menu.append(menu_open)
309 # the recent files menu
310 self.menu_recent = gtk.MenuItem(_('Open recent playlist'))
311 menu.append(self.menu_recent)
312 self.create_recent_files_menu()
314 menu.append(gtk.SeparatorMenuItem())
316 menu_save = gtk.ImageMenuItem(_('Save current playlist'))
317 menu_save.set_image(
318 gtk.image_new_from_stock(gtk.STOCK_SAVE_AS, gtk.ICON_SIZE_MENU))
319 menu_save.connect("activate", self.save_to_playlist_callback)
320 menu.append(menu_save)
322 menu.append(gtk.SeparatorMenuItem())
324 # the settings sub-menu
325 menu_settings = gtk.MenuItem(_('Settings'))
326 menu.append(menu_settings)
328 menu_settings_sub = gtk.Menu()
329 menu_settings.set_submenu(menu_settings_sub)
331 menu_settings_enable_dual_action = gtk.CheckMenuItem(
332 _('Enable dual-action buttons') )
333 settings.attach_checkbutton( menu_settings_enable_dual_action,
334 'enable_dual_action_btn' )
335 menu_settings_sub.append(menu_settings_enable_dual_action)
337 menu_settings_lock_progress = gtk.CheckMenuItem(_('Lock Progress Bar'))
338 settings.attach_checkbutton( menu_settings_lock_progress,
339 'progress_locked' )
340 menu_settings_sub.append(menu_settings_lock_progress)
342 menu_about = gtk.ImageMenuItem(gtk.STOCK_ABOUT)
343 menu_about.connect("activate", self.about_callback)
344 menu.append(menu_about)
346 menu.append(gtk.SeparatorMenuItem())
348 menu_quit = gtk.ImageMenuItem(gtk.STOCK_QUIT)
349 menu_quit.connect("activate", self.destroy)
350 menu.append(menu_quit)
352 return menu
354 def create_recent_files_menu( self ):
355 max_files = settings.max_recent_files
356 self.recent_files = player.playlist.get_recent_files(max_files)
357 menu_recent_sub = gtk.Menu()
359 if len(self.recent_files) > 0:
360 for f in self.recent_files:
361 # don't include the temporary playlist in the file list
362 if f == panucci.PLAYLIST_FILE: continue
363 # don't include non-existant files
364 if not os.path.exists( f ): continue
365 filename, extension = os.path.splitext(os.path.basename(f))
366 menu_item = gtk.MenuItem( filename.replace('_', ' '))
367 menu_item.connect('activate', self.on_recent_file_activate, f)
368 menu_recent_sub.append(menu_item)
369 else:
370 menu_item = gtk.MenuItem(_('No recent files available.'))
371 menu_item.set_sensitive(False)
372 menu_recent_sub.append(menu_item)
374 self.menu_recent.set_submenu(menu_recent_sub)
376 def destroy(self, widget):
377 player.quit()
378 gtk.main_quit()
380 def show_main_window(self):
381 self.main_window.present()
383 def check_queue(self):
384 """ Makes sure the queue is saved if it has been modified
385 True means a new file can be opened
386 False means the user does not want to continue """
388 if not self.__ignore_queue_check and player.playlist.queue_modified:
389 response = dialog(
390 self.main_window, _('Save current playlist'),
391 _('Current playlist has been modified'),
392 _('Opening a new file will replace the current playlist. ') +
393 _('Do you want to save it before creating a new one?'),
394 affirmative_button=gtk.STOCK_SAVE,
395 negative_button=_('Discard changes'))
397 self.__log.debug('Response to "Save Queue?": %s', response)
399 if response is None:
400 return False
401 elif response:
402 return self.save_to_playlist_callback()
403 elif not response:
404 return True
405 else:
406 return False
407 else:
408 return True
410 def open_file_callback(self, widget=None):
411 if self.check_queue():
412 # set __ingnore__queue_check because we already did the check
413 self.__ignore_queue_check = True
414 filename = get_file_from_filechooser(self.main_window)
415 if filename is not None:
416 self._play_file(filename)
418 self.__ignore_queue_check = False
420 def save_to_playlist_callback(self, widget=None):
421 filename = get_file_from_filechooser(
422 self.main_window, save_file=True, save_to='playlist.m3u' )
424 if filename is None:
425 return False
427 if os.path.isfile(filename):
428 response = dialog( self.main_window, _('File already exists'),
429 _('File already exists'),
430 _('The file %s already exists. You can choose another name or '
431 'overwrite the existing file.') % os.path.basename(filename),
432 affirmative_button=gtk.STOCK_SAVE,
433 negative_button=_('Rename file'))
435 if response is None:
436 return None
438 elif response:
439 pass
440 elif not response:
441 return self.save_to_playlist_callback()
443 ext = util.detect_filetype(filename)
444 if not player.playlist.save_to_new_playlist(filename, ext):
445 util.notify(_('Error saving playlist...'))
446 return False
448 return True
450 def __get_fullscreen(self):
451 return self.__window_fullscreen
453 def __set_fullscreen(self, value):
454 if value != self.__window_fullscreen:
455 if value:
456 self.main_window.fullscreen()
457 else:
458 self.main_window.unfullscreen()
460 self.__window_fullscreen = value
461 player.playlist.send_metadata()
463 fullscreen = property( __get_fullscreen, __set_fullscreen )
465 def on_key_press(self, widget, event):
466 if util.platform.MAEMO:
467 if event.keyval == gtk.keysyms.F6:
468 self.fullscreen = not self.fullscreen
470 def on_recent_file_activate(self, widget, filepath):
471 self._play_file(filepath)
473 def on_file_queued(self, filepath, success, notify):
474 if notify:
475 filename = os.path.basename(filepath)
476 if success:
477 self.__log.info(
478 util.notify( '%s added successfully.' % filename ))
479 else:
480 self.__log.error(
481 util.notify( 'Error adding %s to the queue.' % filename))
483 def about_callback(self, widget):
484 dialog = gtk.AboutDialog()
485 dialog.set_website(about_website)
486 dialog.set_website_label(about_website)
487 dialog.set_name(about_name)
488 dialog.set_authors(about_authors)
489 dialog.set_comments(about_text)
490 dialog.set_version(panucci.__version__)
491 dialog.run()
492 dialog.destroy()
494 def _play_file(self, filename, pause_on_load=False):
495 player.playlist.load( os.path.abspath(filename) )
497 if player.playlist.is_empty:
498 return False
500 def handle_headset_button(self, event, button):
501 if event == 'ButtonPressed' and button == 'phone':
502 player.play_pause_toggle()
504 def __select_current_item( self ):
505 # Select the currently playing track in the playlist tab
506 # and switch to it (so we can edit bookmarks, etc.. there)
507 self.__playlist_tab.select_current_item()
508 self.playlist_window.show()
510 ##################################################
511 # PlayerTab
512 ##################################################
513 class PlayerTab(ObservableService, gtk.HBox):
514 """ The tab that holds the player elements """
516 signals = [ 'select-current-item-request', ]
518 def __init__(self, gui_root):
519 self.__log = logging.getLogger('panucci.panucci.PlayerTab')
520 self.__gui_root = gui_root
522 gtk.HBox.__init__(self)
523 ObservableService.__init__(self, self.signals, self.__log)
525 # Timers
526 self.progress_timer_id = None
527 self.volume_timer_id = None
529 self.recent_files = []
530 self.make_player_tab()
531 self.has_coverart = False
532 self.set_volume(settings.volume)
534 #settings.register( 'enable_dual_action_btn_changed',
535 # self.on_dual_action_setting_changed )
536 #settings.register( 'dual_action_button_delay_changed',
537 # self.on_dual_action_setting_changed )
538 #settings.register( 'volume_changed', self.set_volume )
539 #settings.register( 'scrolling_labels_changed', lambda v:
540 # setattr( self.title_label, 'scrolling', v ) )
542 player.register( 'stopped', self.on_player_stopped )
543 player.register( 'playing', self.on_player_playing )
544 player.register( 'paused', self.on_player_paused )
545 player.playlist.register( 'end-of-playlist',
546 self.on_player_end_of_playlist )
547 player.playlist.register( 'new-track-loaded',
548 self.on_player_new_track )
549 player.playlist.register( 'new-metadata-available',
550 self.on_player_new_metadata )
552 def make_player_tab(self):
553 main_vbox = gtk.VBox()
554 main_vbox.set_spacing(6)
555 # add a vbox to self
556 self.pack_start(main_vbox, True, True)
558 # a hbox to hold the cover art and metadata vbox
559 metadata_hbox = gtk.HBox()
560 metadata_hbox.set_spacing(6)
561 main_vbox.pack_start(metadata_hbox, True, False)
563 self.cover_art = gtk.Image()
564 metadata_hbox.pack_start( self.cover_art, False, False )
566 # vbox to hold metadata
567 metadata_vbox = gtk.VBox()
568 metadata_vbox.set_spacing(8)
569 empty_label = gtk.Label()
570 metadata_vbox.pack_start(empty_label, True, True)
571 self.artist_label = gtk.Label('')
572 self.artist_label.set_ellipsize(pango.ELLIPSIZE_END)
573 metadata_vbox.pack_start(self.artist_label, False, False)
574 self.album_label = gtk.Label('')
575 self.album_label.set_ellipsize(pango.ELLIPSIZE_END)
576 metadata_vbox.pack_start(self.album_label, False, False)
577 self.title_label = widgets.ScrollingLabel( '',
578 update_interval=200,
579 pixel_jump=5,
580 delay_btwn_scrolls=5000,
581 delay_halfway=3000 )
582 self.title_label.scrolling = settings.scrolling_labels
583 metadata_vbox.pack_start(self.title_label, False, False)
584 empty_label = gtk.Label()
585 metadata_vbox.pack_start(empty_label, True, True)
586 metadata_hbox.pack_start( metadata_vbox, True, True )
588 progress_eventbox = gtk.EventBox()
589 progress_eventbox.set_events(gtk.gdk.BUTTON_PRESS_MASK)
590 progress_eventbox.connect(
591 'button-press-event', self.on_progressbar_changed )
592 self.progress = gtk.ProgressBar()
593 # make the progress bar more "finger-friendly"
594 if util.platform.FREMANTLE:
595 self.progress.set_size_request(-1, 100)
596 elif util.platform.MAEMO:
597 self.progress.set_size_request(-1, 50)
598 progress_eventbox.add(self.progress)
599 main_vbox.pack_start( progress_eventbox, False, False )
601 # make the button box
602 buttonbox = gtk.HBox()
604 # A wrapper to help create DualActionButtons with the right settings
605 create_da = lambda a, b, c=None, d=None: widgets.DualActionButton(
606 a, b, c, d, settings.dual_action_button_delay,
607 settings.enable_dual_action_btn )
609 self.rrewind_button = create_da(
610 generate_image('media-skip-backward.png'),
611 lambda: self.do_seek(-1*settings.seek_long),
612 generate_image(gtk.STOCK_GOTO_FIRST, True),
613 player.playlist.prev)
614 buttonbox.add(self.rrewind_button)
616 self.rewind_button = create_da(
617 generate_image('media-seek-backward.png'),
618 lambda: self.do_seek(-1*settings.seek_short))
619 buttonbox.add(self.rewind_button)
621 self.play_pause_button = gtk.Button('')
622 image(self.play_pause_button, 'media-playback-start.png')
623 self.play_pause_button.connect( 'clicked',
624 self.on_btn_play_pause_clicked )
625 self.play_pause_button.set_sensitive(False)
626 buttonbox.add(self.play_pause_button)
628 self.forward_button = create_da(
629 generate_image('media-seek-forward.png'),
630 lambda: self.do_seek(settings.seek_short))
631 buttonbox.add(self.forward_button)
633 self.fforward_button = create_da(
634 generate_image('media-skip-forward.png'),
635 lambda: self.do_seek(settings.seek_long),
636 generate_image(gtk.STOCK_GOTO_LAST, True),
637 player.playlist.next)
638 buttonbox.add(self.fforward_button)
640 self.bookmarks_button = create_da(
641 generate_image('bookmark-new.png'),
642 player.add_bookmark_at_current_position,
643 generate_image(gtk.STOCK_JUMP_TO, True),
644 lambda *args: self.notify('select-current-item-request'))
645 buttonbox.add(self.bookmarks_button)
646 self.set_controls_sensitivity(False)
647 main_vbox.pack_start(buttonbox, False, False)
649 if util.platform.MAEMO:
650 self.volume = hildon.VVolumebar()
651 self.volume.set_property('can-focus', False)
652 self.volume.connect('level_changed', self.volume_changed_hildon)
653 self.volume.connect('mute_toggled', self.mute_toggled)
654 self.__gui_root.main_window.connect( 'key-press-event',
655 self.on_key_press )
656 if not util.platform.FREMANTLE:
657 self.pack_start(self.volume, False, True)
659 # Add a button to pop out the volume bar
660 self.volume_button = gtk.ToggleButton('')
661 image(self.volume_button, 'media-speaker.png')
662 self.volume_button.connect('clicked', self.toggle_volumebar)
663 self.volume.connect(
664 'show', lambda x: self.volume_button.set_active(True))
665 self.volume.connect(
666 'hide', lambda x: self.volume_button.set_active(False))
667 if not util.platform.FREMANTLE:
668 buttonbox.add(self.volume_button)
669 self.volume_button.show()
671 # Disable focus for all widgets, so we can use the cursor
672 # keys + enter to directly control our media player, which
673 # is handled by "key-press-event"
674 for w in (
675 self.rrewind_button, self.rewind_button,
676 self.play_pause_button, self.forward_button,
677 self.fforward_button, self.progress,
678 self.bookmarks_button, self.volume_button, ):
679 w.unset_flags(gtk.CAN_FOCUS)
680 else:
681 self.volume = gtk.VolumeButton()
682 self.volume.connect('value-changed', self.volume_changed_gtk)
683 buttonbox.add(self.volume)
684 self.volume.show()
686 self.set_volume(settings.volume)
688 def set_controls_sensitivity(self, sensitive):
689 for button in self.forward_button, self.rewind_button, \
690 self.fforward_button, self.rrewind_button:
692 button.set_sensitive(sensitive)
694 # the play/pause button should always be available except
695 # for when the player starts without a file
696 self.play_pause_button.set_sensitive(True)
698 def on_dual_action_setting_changed( self, *args ):
699 for button in self.forward_button, self.rewind_button, \
700 self.fforward_button, self.rrewind_button, \
701 self.bookmarks_button:
703 button.set_longpress_enabled( settings.enable_dual_action_btn )
704 button.set_duration( settings.dual_action_button_delay )
706 def on_key_press(self, widget, event):
707 if util.platform.MAEMO:
708 if event.keyval == gtk.keysyms.F7: #plus
709 self.set_volume( min( 1, self.get_volume() + 0.10 ))
710 elif event.keyval == gtk.keysyms.F8: #minus
711 self.set_volume( max( 0, self.get_volume() - 0.10 ))
712 elif event.keyval == gtk.keysyms.Left: # seek back
713 self.do_seek( -1 * settings.seek_long )
714 elif event.keyval == gtk.keysyms.Right: # seek forward
715 self.do_seek( settings.seek_long )
716 elif event.keyval == gtk.keysyms.Return: # play/pause
717 self.on_btn_play_pause_clicked()
719 # The following two functions get and set the
720 # volume from the volume control widgets.
721 def get_volume(self):
722 if util.platform.MAEMO:
723 return self.volume.get_level()/100.0
724 else:
725 return self.volume.get_value()
727 def set_volume(self, vol):
728 """ vol is a float from 0 to 1 """
729 assert 0 <= vol <= 1
731 if util.platform.FREMANTLE:
732 # No volume setting on Maemo 5
733 return
735 if util.platform.MAEMO:
736 self.volume.set_level(vol*100.0)
737 else:
738 self.volume.set_value(vol)
740 def __set_volume_hide_timer(self, timeout, force_show=False):
741 if force_show or self.volume_button.get_active():
742 self.volume.show()
743 if self.volume_timer_id is not None:
744 gobject.source_remove(self.volume_timer_id)
745 self.volume_timer_id = None
747 self.volume_timer_id = gobject.timeout_add(
748 1000 * timeout, self.__volume_hide_callback )
750 def __volume_hide_callback(self):
751 self.volume_timer_id = None
752 self.volume.hide()
753 return False
755 def toggle_volumebar(self, widget=None):
756 if self.volume_timer_id is None:
757 self.__set_volume_hide_timer(5)
758 else:
759 self.__volume_hide_callback()
761 def volume_changed_gtk(self, widget, new_value=0.5):
762 settings.volume = new_value
764 def volume_changed_hildon(self, widget):
765 self.__set_volume_hide_timer( 4, force_show=True )
766 settings.volume = widget.get_level()/100.0
768 def mute_toggled(self, widget):
769 if widget.get_mute():
770 settings.volume = 0
771 else:
772 settings.volume = widget.get_level()/100.0
774 def on_player_stopped(self):
775 self.stop_progress_timer()
776 self.set_controls_sensitivity(False)
777 image(self.play_pause_button, 'media-playback-start.png')
779 def on_player_playing(self):
780 self.start_progress_timer()
781 image(self.play_pause_button, 'media-playback-pause.png')
782 self.set_controls_sensitivity(True)
784 def on_player_new_track(self):
785 for widget in [self.title_label,self.artist_label,self.album_label]:
786 widget.set_markup('')
787 widget.hide()
789 self.cover_art.hide()
790 self.has_coverart = False
792 def on_player_new_metadata(self):
793 metadata = player.playlist.get_file_metadata()
794 self.set_metadata(metadata)
796 if not player.playing:
797 position = player.playlist.get_current_position()
798 estimated_length = metadata.get('length', 0)
799 self.set_progress_callback( position, estimated_length )
801 def on_player_paused( self, position, duration ):
802 self.stop_progress_timer() # This should save some power
803 self.set_progress_callback( position, duration )
804 image(self.play_pause_button, 'media-playback-start.png')
806 def on_player_end_of_playlist(self, loop):
807 pass
809 def reset_progress(self):
810 self.progress.set_fraction(0)
811 self.set_progress_callback(0,0)
813 def set_progress_callback(self, time_elapsed, total_time):
814 """ times must be in nanoseconds """
815 time_string = "%s / %s" % ( util.convert_ns(time_elapsed),
816 util.convert_ns(total_time) )
817 self.progress.set_text( time_string )
818 fraction = float(time_elapsed) / float(total_time) if total_time else 0
819 self.progress.set_fraction( fraction )
821 def on_progressbar_changed(self, widget, event):
822 if ( not settings.progress_locked and
823 event.type == gtk.gdk.BUTTON_PRESS and event.button == 1 ):
824 new_fraction = event.x/float(widget.get_allocation().width)
825 resp = player.do_seek(percent=new_fraction)
826 if resp:
827 # Preemptively update the progressbar to make seeking smoother
828 self.set_progress_callback( *resp )
830 def on_btn_play_pause_clicked(self, widget=None):
831 player.play_pause_toggle()
833 def progress_timer_callback( self ):
834 if player.playing and not player.seeking:
835 pos_int, dur_int = player.get_position_duration()
836 # This prevents bogus values from being set while seeking
837 if ( pos_int > 10**9 ) and ( dur_int > 10**9 ):
838 self.set_progress_callback( pos_int, dur_int )
839 return True
841 def start_progress_timer( self ):
842 if self.progress_timer_id is not None:
843 self.stop_progress_timer()
845 self.progress_timer_id = gobject.timeout_add(
846 1000, self.progress_timer_callback )
848 def stop_progress_timer( self ):
849 if self.progress_timer_id is not None:
850 gobject.source_remove( self.progress_timer_id )
851 self.progress_timer_id = None
853 def get_coverart_size( self ):
854 if util.platform.MAEMO:
855 if self.__gui_root.fullscreen:
856 size = coverart_sizes['maemo fullscreen']
857 else:
858 size = coverart_sizes['maemo']
859 else:
860 size = coverart_sizes['normal']
862 return size, size
864 def set_coverart( self, pixbuf ):
865 self.cover_art.set_from_pixbuf(pixbuf)
866 self.cover_art.show()
867 self.has_coverart = True
869 def set_metadata( self, tag_message ):
870 tags = { 'title': self.title_label, 'artist': self.artist_label,
871 'album': self.album_label }
873 # set the coverart
874 if tag_message.has_key('image') and tag_message['image'] is not None:
875 value = tag_message['image']
877 pbl = gtk.gdk.PixbufLoader()
878 try:
879 pbl.write(value)
880 pbl.close()
882 x, y = self.get_coverart_size()
883 pixbuf = pbl.get_pixbuf()
884 pixbuf = pixbuf.scale_simple( x, y, gtk.gdk.INTERP_BILINEAR )
885 self.set_coverart(pixbuf)
886 except Exception, e:
887 self.__log.exception('Error setting coverart...')
889 # set the text metadata
890 for tag,value in tag_message.iteritems():
891 if tags.has_key(tag) and value is not None and value.strip():
892 tags[tag].set_markup('<big>'+value+'</big>')
893 tags[tag].set_alignment( 0.5*int(not self.has_coverart), 0.5)
894 tags[tag].show()
896 if tag == 'title':
897 # make the title bold
898 tags[tag].set_markup('<b><big>'+value+'</big></b>')
900 if not util.platform.MAEMO:
901 value += ' - Panucci'
903 self.__gui_root.main_window.set_title( value )
905 def do_seek(self, seek_amount):
906 resp = player.do_seek(from_current=seek_amount*10**9)
907 if resp:
908 # Preemptively update the progressbar to make seeking smoother
909 self.set_progress_callback( *resp )
913 ##################################################
914 # PlaylistTab
915 ##################################################
916 class PlaylistTab(gtk.VBox):
917 def __init__(self, main_window):
918 gtk.VBox.__init__(self)
919 self.__log = logging.getLogger('panucci.panucci.BookmarksWindow')
920 self.main = main_window
922 self.__model = gtk.TreeStore(
923 # uid, name, position
924 gobject.TYPE_STRING, gobject.TYPE_STRING, gobject.TYPE_STRING )
926 self.set_spacing(5)
927 self.treeview = gtk.TreeView()
928 self.treeview.set_model(self.__model)
929 self.treeview.set_headers_visible(True)
930 tree_selection = self.treeview.get_selection()
931 # This breaks drag and drop, only use single selection for now
932 # tree_selection.set_mode(gtk.SELECTION_MULTIPLE)
933 tree_selection.connect('changed', self.tree_selection_changed)
935 # The tree lines look nasty on maemo
936 if util.platform.DESKTOP:
937 self.treeview.set_enable_tree_lines(True)
938 self.update_model()
940 ncol = gtk.TreeViewColumn(_('Name'))
941 ncell = gtk.CellRendererText()
942 ncell.set_property('ellipsize', pango.ELLIPSIZE_END)
943 ncell.set_property('editable', True)
944 ncell.connect('edited', self.label_edited)
945 ncol.set_expand(True)
946 ncol.pack_start(ncell)
947 ncol.add_attribute(ncell, 'text', 1)
949 tcol = gtk.TreeViewColumn(_('Position'))
950 tcell = gtk.CellRendererText()
951 tcol.pack_start(tcell)
952 tcol.add_attribute(tcell, 'text', 2)
954 self.treeview.append_column(ncol)
955 self.treeview.append_column(tcol)
956 self.treeview.connect('drag-data-received', self.drag_data_recieved)
957 self.treeview.connect('drag_data_get', self.drag_data_get_data)
959 treeview_targets = [
960 ( 'playlist_row_data', gtk.TARGET_SAME_WIDGET, 0 ) ]
962 self.treeview.enable_model_drag_source(
963 gtk.gdk.BUTTON1_MASK, treeview_targets, gtk.gdk.ACTION_COPY )
965 self.treeview.enable_model_drag_dest(
966 treeview_targets, gtk.gdk.ACTION_COPY )
968 sw = gtk.ScrolledWindow()
969 sw.set_policy(gtk.POLICY_AUTOMATIC, gtk.POLICY_AUTOMATIC)
970 sw.set_shadow_type(gtk.SHADOW_IN)
971 sw.add(self.treeview)
972 self.add(sw)
974 self.hbox = gtk.HBox()
976 self.add_button = gtk.Button(gtk.STOCK_NEW)
977 self.add_button.set_use_stock(True)
978 set_stock_button_text( self.add_button, _('Add File') )
979 self.add_button.connect('clicked', self.add_file)
980 self.hbox.pack_start(self.add_button, True, True)
982 self.dir_button = gtk.Button(gtk.STOCK_OPEN)
983 self.dir_button.set_use_stock(True)
984 set_stock_button_text( self.dir_button, _('Add Directory') )
985 self.dir_button.connect('clicked', self.add_directory)
986 self.hbox.pack_start(self.dir_button, True, True)
988 self.remove_button = widgets.DualActionButton(
989 generate_image(gtk.STOCK_REMOVE, True),
990 self.remove_bookmark,
991 generate_image(gtk.STOCK_CANCEL, True),
992 lambda *a: player.playlist.reset_playlist() )
993 #self.remove_button.set_use_stock(True)
994 #self.remove_button.connect('clicked', self.remove_bookmark)
995 self.hbox.pack_start(self.remove_button, True, True)
997 self.jump_button = gtk.Button(gtk.STOCK_JUMP_TO)
998 self.jump_button.set_use_stock(True)
999 self.jump_button.connect('clicked', self.jump_bookmark)
1000 self.hbox.pack_start(self.jump_button, True, True)
1002 self.info_button = gtk.Button()
1003 self.info_button.add(
1004 gtk.image_new_from_stock(gtk.STOCK_INFO, gtk.ICON_SIZE_BUTTON))
1005 self.info_button.connect('clicked', self.show_playlist_item_details)
1006 self.hbox.pack_start(self.info_button, True, True)
1008 self.pack_start(self.hbox, False, True)
1010 player.playlist.register( 'file_queued',
1011 lambda x,y,z: self.update_model() )
1012 player.playlist.register( 'bookmark_added', self.on_bookmark_added )
1014 self.show_all()
1016 def tree_selection_changed(self, treeselection):
1017 count = treeselection.count_selected_rows()
1018 self.remove_button.set_sensitive(count > 0)
1019 self.jump_button.set_sensitive(count == 1)
1020 self.info_button.set_sensitive(count == 1)
1022 def drag_data_get_data(
1023 self, treeview, context, selection, target_id, timestamp):
1025 treeselection = treeview.get_selection()
1026 model, iter = treeselection.get_selected()
1027 # only allow moving around top-level parents
1028 if model.iter_parent(iter) is None:
1029 # send the path of the selected row
1030 data = model.get_string_from_iter(iter)
1031 selection.set(selection.target, 8, data)
1032 else:
1033 self.__log.debug("Can't move children...")
1035 def drag_data_recieved(
1036 self, treeview, context, x, y, selection, info, timestamp):
1038 drop_info = treeview.get_dest_row_at_pos(x, y)
1040 # TODO: If user drags the row past the last row, drop_info is None
1041 # I'm not sure if it's safe to simply assume that None is
1042 # euqivalent to the last row...
1043 if None not in [ drop_info and selection.data ]:
1044 model = treeview.get_model()
1045 path, position = drop_info
1047 from_iter = model.get_iter_from_string(selection.data)
1049 # make sure the to_iter doesn't have a parent
1050 to_iter = model.get_iter(path)
1051 if model.iter_parent(to_iter) is not None:
1052 to_iter = model.iter_parent(to_iter)
1054 from_row = model.get_path(from_iter)[0]
1055 to_row = path[0]
1057 if ( position == gtk.TREE_VIEW_DROP_BEFORE or
1058 position == gtk.TREE_VIEW_DROP_INTO_OR_BEFORE ):
1059 model.move_before( from_iter, to_iter )
1060 to_row = to_row - 1 if from_row < to_row else to_row
1061 elif ( position == gtk.TREE_VIEW_DROP_AFTER or
1062 position == gtk.TREE_VIEW_DROP_INTO_OR_AFTER ):
1063 model.move_after( from_iter, to_iter )
1064 to_row = to_row + 1 if from_row > to_row else to_row
1065 else:
1066 self.__log.debug('Drop not supported: %s', position)
1068 # don't do anything if we're not actually moving rows around
1069 if from_row != to_row:
1070 player.playlist.move_item( from_row, to_row )
1072 else:
1073 self.__log.debug('No drop_data or selection.data available')
1075 def update_model(self):
1076 plist = player.playlist
1077 path_info = self.treeview.get_path_at_pos(0,0)
1078 path = path_info[0] if path_info is not None else None
1080 self.__model.clear()
1082 # build the tree
1083 for item, data in plist.get_playlist_item_ids():
1084 parent = self.__model.append(None, (item, data.get('title'), None))
1086 for bid, bname, bpos in plist.get_bookmarks_from_item_id( item ):
1087 nice_bpos = util.convert_ns(bpos)
1088 self.__model.append( parent, (bid, bname, nice_bpos) )
1090 self.treeview.expand_all()
1092 if path is not None:
1093 self.treeview.scroll_to_cell(path)
1095 def label_edited(self, cellrenderer, path, new_text):
1096 iter = self.__model.get_iter(path)
1097 old_text = self.__model.get_value(iter, 1)
1099 if new_text.strip() and old_text != new_text:
1100 # this loop will only run once, because only one cell can be
1101 # edited at a time, we use it to get the item and bookmark ids
1102 for m, bkmk_id, biter, item_id, iiter in self.__cur_selection():
1103 self.__model.set_value(iter, 1, new_text)
1104 player.playlist.update_bookmark(
1105 item_id, bkmk_id, name=new_text )
1106 else:
1107 self.__model.set_value(iter, 1, old_text)
1109 def on_bookmark_added(self, parent_id, bookmark_name, position):
1110 util.notify(_('Bookmark added: %s') % bookmark_name)
1111 self.update_model()
1113 def add_file(self, widget):
1114 filename = get_file_from_filechooser(self.main.main_window)
1115 if filename is not None:
1116 player.playlist.append(filename)
1118 def add_directory(self, widget):
1119 directory = get_file_from_filechooser(
1120 self.main.main_window, folder=True )
1121 if directory is not None:
1122 player.playlist.load_directory(directory, append=True)
1124 def __cur_selection(self):
1125 selection = self.treeview.get_selection()
1126 model, bookmark_paths = selection.get_selected_rows()
1128 # Convert the paths to gtk.TreeRowReference objects, because we
1129 # might modify the model while this generator is running
1130 bookmark_refs = [gtk.TreeRowReference(model, p) for p in bookmark_paths]
1132 for reference in bookmark_refs:
1133 bookmark_iter = model.get_iter(reference.get_path())
1134 item_iter = model.iter_parent(bookmark_iter)
1136 # bookmark_iter is actually an item_iter
1137 if item_iter is None:
1138 item_iter = bookmark_iter
1139 item_id = model.get_value(item_iter, 0)
1140 bookmark_id, bookmark_iter = None, None
1141 else:
1142 bookmark_id = model.get_value(bookmark_iter, 0)
1143 item_id = model.get_value(item_iter, 0)
1145 yield model, bookmark_id, bookmark_iter, item_id, item_iter
1147 def remove_bookmark(self, w=None):
1148 for model, bkmk_id, bkmk_iter, item_id, item_iter in self.__cur_selection():
1149 player.playlist.remove_bookmark( item_id, bkmk_id )
1150 if bkmk_iter is not None:
1151 model.remove(bkmk_iter)
1152 elif item_iter is not None:
1153 model.remove(item_iter)
1155 def select_current_item(self):
1156 model = self.treeview.get_model()
1157 selection = self.treeview.get_selection()
1158 current_item_id = str(player.playlist.get_current_item())
1159 for row in iter(model):
1160 if model.get_value(row.iter, 0) == current_item_id:
1161 selection.unselect_all()
1162 self.treeview.set_cursor(row.path)
1163 self.treeview.scroll_to_cell(row.path, use_align=True)
1164 break
1166 def show_playlist_item_details(self, w):
1167 selection = self.treeview.get_selection()
1168 if selection.count_selected_rows() == 1:
1169 selected = self.__cur_selection().next()
1170 model, bkmk_id, bkmk_iter, item_id, item_iter = selected
1171 playlist_item = player.playlist.get_item_by_id(item_id)
1172 PlaylistItemDetails(self.main, playlist_item)
1174 def jump_bookmark(self, w):
1175 selected = list(self.__cur_selection())
1176 if len(selected) == 1:
1177 # It should be guranteed by the fact that we only enable the
1178 # "Jump to" button when the selection count equals 1.
1179 model, bkmk_id, bkmk_iter, item_id, item_iter = selected.pop(0)
1180 player.playlist.load_from_bookmark_id(item_id, bkmk_id)
1182 # FIXME: The player/playlist should be able to take care of this
1183 if not player.playing:
1184 player.play()
1187 ##################################################
1188 # PlaylistItemDetails
1189 ##################################################
1190 class PlaylistItemDetails(gtk.Dialog):
1191 def __init__(self, main, playlist_item):
1192 gtk.Dialog.__init__( self, _('Playlist item details'),
1193 main.main_window, gtk.DIALOG_MODAL,
1194 (gtk.STOCK_CLOSE, gtk.RESPONSE_OK))
1196 self.main = main
1197 self.fill(playlist_item)
1198 self.set_has_separator(False)
1199 self.set_resizable(False)
1200 self.show_all()
1201 self.run()
1202 self.destroy()
1204 def fill(self, playlist_item):
1205 t = gtk.Table(10, 2)
1206 self.vbox.pack_start(t, expand=False)
1208 metadata = playlist_item.metadata
1210 t.attach(gtk.Label(_('Custom title:')), 0, 1, 0, 1)
1211 t.attach(gtk.Label(_('ID:')), 0, 1, 1, 2)
1212 t.attach(gtk.Label(_('Playlist ID:')), 0, 1, 2, 3)
1213 t.attach(gtk.Label(_('Filepath:')), 0, 1, 3, 4)
1215 row_num = 4
1216 for key in metadata:
1217 if metadata[key] is not None:
1218 t.attach( gtk.Label(key.capitalize()+':'),
1219 0, 1, row_num, row_num+1 )
1220 row_num += 1
1222 t.foreach(lambda x, y: x.set_alignment(1, 0.5), None)
1223 t.foreach(lambda x, y: x.set_markup('<b>%s</b>' % x.get_label()), None)
1225 t.attach(gtk.Label(playlist_item.title or _('<not modified>')),1,2,0,1)
1226 t.attach(gtk.Label(str(playlist_item)), 1, 2, 1, 2)
1227 t.attach(gtk.Label(playlist_item.playlist_id), 1, 2, 2, 3)
1228 t.attach(gtk.Label(playlist_item.filepath), 1, 2, 3, 4)
1230 row_num = 4
1231 for key in metadata:
1232 value = metadata[key]
1233 if key == 'length':
1234 value = util.convert_ns(value)
1235 if metadata[key] is not None:
1236 t.attach( gtk.Label( str(value) or _('<not set>')),
1237 1, 2, row_num, row_num+1)
1238 row_num += 1
1240 t.foreach(lambda x, y: x.get_alignment() == (0.5, 0.5) and \
1241 x.set_alignment(0, 0.5), None)
1243 t.set_border_width(8)
1244 t.set_row_spacings(4)
1245 t.set_col_spacings(8)
1247 l = gtk.ListStore(str, str)
1248 t = gtk.TreeView(l)
1249 cr = gtk.CellRendererText()
1250 cr.set_property('ellipsize', pango.ELLIPSIZE_END)
1251 c = gtk.TreeViewColumn(_('Title'), cr, text=0)
1252 c.set_expand(True)
1253 t.append_column(c)
1254 c = gtk.TreeViewColumn(_('Time'), gtk.CellRendererText(), text=1)
1255 t.append_column(c)
1256 playlist_item.load_bookmarks()
1257 for bookmark in playlist_item.bookmarks:
1258 l.append([bookmark.bookmark_name, \
1259 util.convert_ns(bookmark.seek_position)])
1261 sw = gtk.ScrolledWindow()
1262 sw.set_shadow_type(gtk.SHADOW_IN)
1263 sw.add(t)
1264 sw.set_policy(gtk.POLICY_AUTOMATIC, gtk.POLICY_AUTOMATIC)
1265 e = gtk.Expander(_('Bookmarks'))
1266 e.add(sw)
1267 self.vbox.pack_start(e)
1270 def run(filename=None):
1271 PanucciGUI( filename )
1272 gtk.main()
1274 if __name__ == '__main__':
1275 log.error( 'Use the "panucci" executable to run this program.' )
1276 log.error( 'Exiting...' )
1277 sys.exit(1)