Clean-up: Remove all volume controls
[panucci.git] / src / panucci / main.py
blob27e5d6c9f464d5864ea26fb0ea0642f322a213a4
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.FREMANTLE:
161 if save_file:
162 dlg = gobject.new(hildon.FileChooserDialog, \
163 action=gtk.FILE_CHOOSER_ACTION_SAVE)
164 else:
165 dlg = gobject.new(hildon.FileChooserDialog, \
166 action=gtk.FILE_CHOOSER_ACTION_OPEN)
167 elif platform.MAEMO:
168 if save_file:
169 args = ( toplevel_window, gtk.FILE_CHOOSER_ACTION_SAVE )
170 else:
171 args = ( toplevel_window, open_action )
173 dlg = hildon.FileChooserDialog( *args )
174 else:
175 if save_file:
176 args = ( _('Select file to save playlist to'), None,
177 gtk.FILE_CHOOSER_ACTION_SAVE,
178 (( gtk.STOCK_CANCEL, gtk.RESPONSE_REJECT,
179 gtk.STOCK_SAVE, gtk.RESPONSE_OK )) )
180 else:
181 args = ( _('Select podcast or audiobook'), None, open_action,
182 (( gtk.STOCK_CANCEL, gtk.RESPONSE_REJECT,
183 gtk.STOCK_OPEN, gtk.RESPONSE_OK )) )
185 dlg = gtk.FileChooserDialog(*args)
187 current_folder = os.path.expanduser(settings.last_folder)
189 if current_folder is not None and os.path.isdir(current_folder):
190 dlg.set_current_folder(current_folder)
192 if save_file and save_to is not None:
193 dlg.set_current_name(save_to)
195 if dlg.run() == gtk.RESPONSE_OK:
196 filename = dlg.get_filename()
197 settings.last_folder = dlg.get_current_folder()
198 else:
199 filename = None
201 dlg.destroy()
202 return filename
204 def set_stock_button_text( button, text ):
205 alignment = button.get_child()
206 hbox = alignment.get_child()
207 image, label = hbox.get_children()
208 label.set_text(text)
210 ##################################################
211 # PanucciGUI
212 ##################################################
213 class PanucciGUI(object):
214 """ The object that holds the entire panucci gui """
216 def __init__(self, filename=None):
217 self.__log = logging.getLogger('panucci.panucci.PanucciGUI')
218 interface.register_gui(self)
220 # Build the base ui (window and menubar)
221 if platform.MAEMO:
222 self.app = hildon.Program()
223 window = hildon.Window()
224 self.app.add_window(window)
225 else:
226 window = gtk.Window(gtk.WINDOW_TOPLEVEL)
228 self.main_window = window
229 window.set_title('Panucci')
230 self.window_icon = find_image('panucci.png')
231 if self.window_icon is not None:
232 window.set_icon_from_file( self.window_icon )
233 window.set_default_size(400, -1)
234 window.set_border_width(0)
235 window.connect("destroy", self.destroy)
237 # Add the tabs (they are private to prevent us from trying to do
238 # something like gui_root.player_tab.some_function() from inside
239 # playlist_tab or vice-versa)
240 self.__player_tab = PlayerTab(self)
241 self.__playlist_tab = PlaylistTab(self)
243 self.playlist_window = gtk.Window(gtk.WINDOW_TOPLEVEL)
244 self.playlist_window.connect('delete-event', gtk.Widget.hide_on_delete)
245 self.playlist_window.set_title(_('Panucci playlist'))
246 self.playlist_window.set_transient_for(self.main_window)
247 self.playlist_window.add(self.__playlist_tab)
249 self.create_actions()
251 if platform.MAEMO:
252 window.set_menu(self.create_menu())
253 window.add(self.__player_tab)
254 else:
255 menu_vbox = gtk.VBox()
256 menu_vbox.set_spacing(0)
257 window.add(menu_vbox)
258 menu_bar = gtk.MenuBar()
259 self.create_desktop_menu(menu_bar)
260 menu_vbox.pack_start(menu_bar, False, False, 0)
261 menu_bar.show()
262 menu_vbox.pack_end(self.__player_tab, True, True, 6)
264 # Tie it all together!
265 self.__ignore_queue_check = False
266 self.__window_fullscreen = False
268 if platform.MAEMO and interface.headset_device is not None:
269 # Enable play/pause with headset button
270 interface.headset_device.connect_to_signal(
271 'Condition', self.handle_headset_button )
273 self.main_window.connect('key-press-event', self.on_key_press)
274 player.playlist.register( 'file_queued', self.on_file_queued )
276 player.playlist.register( 'playlist-to-be-overwritten',
277 self.check_queue )
278 self.__player_tab.register( 'select-current-item-request',
279 self.__select_current_item )
281 self.main_window.show_all()
283 # this should be done when the gui is ready
284 player.init(filepath=filename)
286 def create_actions(self):
287 self.action_open = gtk.Action('open', _('Open'), _('Open a file or playlist'), gtk.STOCK_OPEN)
288 self.action_open.connect('activate', self.open_file_callback)
289 self.action_save = gtk.Action('save', _('Save playlist'), _('Save current playlist to file'), gtk.STOCK_SAVE_AS)
290 self.action_save.connect('activate', self.save_to_playlist_callback)
291 self.action_playlist = gtk.Action('playlist', _('Playlist'), _('Open the current playlist'), None)
292 self.action_playlist.connect('activate', lambda a: self.playlist_window.show())
293 self.action_about = gtk.Action('about', _('About Panucci'), _('Show application version'), gtk.STOCK_ABOUT)
294 self.action_about.connect('activate', self.about_callback)
295 self.action_quit = gtk.Action('quit', _('Quit'), _('Close Panucci'), gtk.STOCK_QUIT)
296 self.action_quit.connect('activate', self.destroy)
298 def create_desktop_menu(self, menu_bar):
299 file_menu_item = gtk.MenuItem(_('File'))
300 file_menu = gtk.Menu()
301 file_menu.append(self.action_open.create_menu_item())
302 file_menu.append(self.action_save.create_menu_item())
303 file_menu.append(gtk.SeparatorMenuItem())
304 file_menu.append(self.action_quit.create_menu_item())
305 file_menu_item.set_submenu(file_menu)
306 menu_bar.append(file_menu_item)
308 tools_menu_item = gtk.MenuItem(_('Tools'))
309 tools_menu = gtk.Menu()
310 tools_menu.append(self.action_playlist.create_menu_item())
311 tools_menu_item.set_submenu(tools_menu)
312 menu_bar.append(tools_menu_item)
314 help_menu_item = gtk.MenuItem(_('Help'))
315 help_menu = gtk.Menu()
316 help_menu.append(self.action_about.create_menu_item())
317 help_menu_item.set_submenu(help_menu)
318 menu_bar.append(help_menu_item)
320 def create_menu(self):
321 # the main menu
322 menu = gtk.Menu()
324 menu_open = gtk.ImageMenuItem(_('Open playlist'))
325 menu_open.set_image(
326 gtk.image_new_from_stock(gtk.STOCK_OPEN, gtk.ICON_SIZE_MENU))
327 menu_open.connect("activate", self.open_file_callback)
328 menu.append(menu_open)
330 # the recent files menu
331 self.menu_recent = gtk.MenuItem(_('Open recent playlist'))
332 menu.append(self.menu_recent)
333 self.create_recent_files_menu()
335 menu.append(gtk.SeparatorMenuItem())
337 menu_save = gtk.ImageMenuItem(_('Save current playlist'))
338 menu_save.set_image(
339 gtk.image_new_from_stock(gtk.STOCK_SAVE_AS, gtk.ICON_SIZE_MENU))
340 menu_save.connect("activate", self.save_to_playlist_callback)
341 menu.append(menu_save)
343 menu.append(gtk.SeparatorMenuItem())
345 # the settings sub-menu
346 menu_settings = gtk.MenuItem(_('Settings'))
347 menu.append(menu_settings)
349 menu_settings_sub = gtk.Menu()
350 menu_settings.set_submenu(menu_settings_sub)
352 menu_settings_enable_dual_action = gtk.CheckMenuItem(
353 _('Enable dual-action buttons') )
354 settings.attach_checkbutton( menu_settings_enable_dual_action,
355 'enable_dual_action_btn' )
356 menu_settings_sub.append(menu_settings_enable_dual_action)
358 menu_settings_lock_progress = gtk.CheckMenuItem(_('Lock Progress Bar'))
359 settings.attach_checkbutton( menu_settings_lock_progress,
360 'progress_locked' )
361 menu_settings_sub.append(menu_settings_lock_progress)
363 menu_about = gtk.ImageMenuItem(gtk.STOCK_ABOUT)
364 menu_about.connect("activate", self.about_callback)
365 menu.append(menu_about)
367 menu.append(gtk.SeparatorMenuItem())
369 menu_quit = gtk.ImageMenuItem(gtk.STOCK_QUIT)
370 menu_quit.connect("activate", self.destroy)
371 menu.append(menu_quit)
373 return menu
375 def create_recent_files_menu( self ):
376 max_files = settings.max_recent_files
377 self.recent_files = player.playlist.get_recent_files(max_files)
378 menu_recent_sub = gtk.Menu()
380 if len(self.recent_files) > 0:
381 for f in self.recent_files:
382 # don't include the temporary playlist in the file list
383 if f == panucci.PLAYLIST_FILE: continue
384 # don't include non-existant files
385 if not os.path.exists( f ): continue
386 filename, extension = os.path.splitext(os.path.basename(f))
387 menu_item = gtk.MenuItem( filename.replace('_', ' '))
388 menu_item.connect('activate', self.on_recent_file_activate, f)
389 menu_recent_sub.append(menu_item)
390 else:
391 menu_item = gtk.MenuItem(_('No recent files available.'))
392 menu_item.set_sensitive(False)
393 menu_recent_sub.append(menu_item)
395 self.menu_recent.set_submenu(menu_recent_sub)
397 def notify(self, message):
398 """ Sends a notification using pynotify, returns message """
399 if platform.DESKTOP and have_pynotify:
400 icon = find_image('panucci_64x64.png')
401 notification = pynotify.Notification(self.main_window.get_title(), message, icon)
402 notification.show()
403 elif platform.FREMANTLE:
404 hildon.hildon_banner_show_information(self.main_window, \
405 '', message)
406 elif platform.MAEMO:
407 # Note: This won't work if we're not in the gtk main loop
408 markup = '<b>%s</b>\n<small>%s</small>' % (self.main_window.get_title(), message)
409 hildon.hildon_banner_show_information_with_markup(self.main_window, None, markup)
411 def destroy(self, widget):
412 player.quit()
413 gtk.main_quit()
415 def show_main_window(self):
416 self.main_window.present()
418 def check_queue(self):
419 """ Makes sure the queue is saved if it has been modified
420 True means a new file can be opened
421 False means the user does not want to continue """
423 if not self.__ignore_queue_check and player.playlist.queue_modified:
424 response = dialog(
425 self.main_window, _('Save current playlist'),
426 _('Current playlist has been modified'),
427 _('Opening a new file will replace the current playlist. ') +
428 _('Do you want to save it before creating a new one?'),
429 affirmative_button=gtk.STOCK_SAVE,
430 negative_button=_('Discard changes'))
432 self.__log.debug('Response to "Save Queue?": %s', response)
434 if response is None:
435 return False
436 elif response:
437 return self.save_to_playlist_callback()
438 elif not response:
439 return True
440 else:
441 return False
442 else:
443 return True
445 def open_file_callback(self, widget=None):
446 if self.check_queue():
447 # set __ingnore__queue_check because we already did the check
448 self.__ignore_queue_check = True
449 filename = get_file_from_filechooser(self.main_window)
450 if filename is not None:
451 self._play_file(filename)
453 self.__ignore_queue_check = False
455 def save_to_playlist_callback(self, widget=None):
456 filename = get_file_from_filechooser(
457 self.main_window, save_file=True, save_to='playlist.m3u' )
459 if filename is None:
460 return False
462 if os.path.isfile(filename):
463 response = dialog( self.main_window, _('File already exists'),
464 _('File already exists'),
465 _('The file %s already exists. You can choose another name or '
466 'overwrite the existing file.') % os.path.basename(filename),
467 affirmative_button=gtk.STOCK_SAVE,
468 negative_button=_('Rename file'))
470 if response is None:
471 return None
473 elif response:
474 pass
475 elif not response:
476 return self.save_to_playlist_callback()
478 ext = util.detect_filetype(filename)
479 if not player.playlist.save_to_new_playlist(filename, ext):
480 self.notify(_('Error saving playlist...'))
481 return False
483 return True
485 def __get_fullscreen(self):
486 return self.__window_fullscreen
488 def __set_fullscreen(self, value):
489 if value != self.__window_fullscreen:
490 if value:
491 self.main_window.fullscreen()
492 else:
493 self.main_window.unfullscreen()
495 self.__window_fullscreen = value
496 player.playlist.send_metadata()
498 fullscreen = property( __get_fullscreen, __set_fullscreen )
500 def on_key_press(self, widget, event):
501 if platform.MAEMO:
502 if event.keyval == gtk.keysyms.F6:
503 self.fullscreen = not self.fullscreen
505 def on_recent_file_activate(self, widget, filepath):
506 self._play_file(filepath)
508 def on_file_queued(self, filepath, success, notify):
509 if notify:
510 filename = os.path.basename(filepath)
511 if success:
512 self.__log.info(
513 self.notify( '%s added successfully.' % filename ))
514 else:
515 self.__log.error(
516 self.notify( 'Error adding %s to the queue.' % filename))
518 def about_callback(self, widget):
519 dialog = gtk.AboutDialog()
520 dialog.set_transient_for(self.main_window)
521 dialog.set_website(about_website)
522 dialog.set_website_label(about_website)
523 dialog.set_name(about_name)
524 dialog.set_authors(about_authors)
525 dialog.set_comments(about_text)
526 dialog.set_version(panucci.__version__)
527 dialog.run()
528 dialog.destroy()
530 def _play_file(self, filename, pause_on_load=False):
531 player.playlist.load( os.path.abspath(filename) )
533 if player.playlist.is_empty:
534 return False
536 def handle_headset_button(self, event, button):
537 if event == 'ButtonPressed' and button == 'phone':
538 player.play_pause_toggle()
540 def __select_current_item( self ):
541 # Select the currently playing track in the playlist tab
542 # and switch to it (so we can edit bookmarks, etc.. there)
543 self.__playlist_tab.select_current_item()
544 self.playlist_window.show()
546 ##################################################
547 # PlayerTab
548 ##################################################
549 class PlayerTab(ObservableService, gtk.HBox):
550 """ The tab that holds the player elements """
552 signals = [ 'select-current-item-request', ]
554 def __init__(self, gui_root):
555 self.__log = logging.getLogger('panucci.panucci.PlayerTab')
556 self.__gui_root = gui_root
558 gtk.HBox.__init__(self)
559 ObservableService.__init__(self, self.signals, self.__log)
561 # Timers
562 self.progress_timer_id = None
564 self.recent_files = []
565 self.make_player_tab()
566 self.has_coverart = False
568 #settings.register( 'enable_dual_action_btn_changed',
569 # self.on_dual_action_setting_changed )
570 #settings.register( 'dual_action_button_delay_changed',
571 # self.on_dual_action_setting_changed )
572 #settings.register( 'scrolling_labels_changed', lambda v:
573 # setattr( self.title_label, 'scrolling', v ) )
575 player.register( 'stopped', self.on_player_stopped )
576 player.register( 'playing', self.on_player_playing )
577 player.register( 'paused', self.on_player_paused )
578 player.playlist.register( 'end-of-playlist',
579 self.on_player_end_of_playlist )
580 player.playlist.register( 'new-track-loaded',
581 self.on_player_new_track )
582 player.playlist.register( 'new-metadata-available',
583 self.on_player_new_metadata )
585 def make_player_tab(self):
586 main_vbox = gtk.VBox()
587 main_vbox.set_spacing(6)
588 # add a vbox to self
589 self.pack_start(main_vbox, True, True)
591 # a hbox to hold the cover art and metadata vbox
592 metadata_hbox = gtk.HBox()
593 metadata_hbox.set_spacing(6)
594 main_vbox.pack_start(metadata_hbox, True, False)
596 self.cover_art = gtk.Image()
597 metadata_hbox.pack_start( self.cover_art, False, False )
599 # vbox to hold metadata
600 metadata_vbox = gtk.VBox()
601 metadata_vbox.set_spacing(8)
602 empty_label = gtk.Label()
603 metadata_vbox.pack_start(empty_label, True, True)
604 self.artist_label = gtk.Label('')
605 self.artist_label.set_ellipsize(pango.ELLIPSIZE_END)
606 metadata_vbox.pack_start(self.artist_label, False, False)
607 self.album_label = gtk.Label('')
608 self.album_label.set_ellipsize(pango.ELLIPSIZE_END)
609 metadata_vbox.pack_start(self.album_label, False, False)
610 self.title_label = widgets.ScrollingLabel('',
611 update_interval=100,
612 pixel_jump=1,
613 delay_btwn_scrolls=5000,
614 delay_halfway=3000)
615 self.title_label.scrolling = settings.scrolling_labels
616 metadata_vbox.pack_start(self.title_label, False, False)
617 empty_label = gtk.Label()
618 metadata_vbox.pack_start(empty_label, True, True)
619 metadata_hbox.pack_start( metadata_vbox, True, True )
621 progress_eventbox = gtk.EventBox()
622 progress_eventbox.set_events(gtk.gdk.BUTTON_PRESS_MASK)
623 progress_eventbox.connect(
624 'button-press-event', self.on_progressbar_changed )
625 self.progress = gtk.ProgressBar()
626 # make the progress bar more "finger-friendly"
627 if platform.FREMANTLE:
628 self.progress.set_size_request(-1, 100)
629 elif platform.MAEMO:
630 self.progress.set_size_request(-1, 50)
631 progress_eventbox.add(self.progress)
632 main_vbox.pack_start( progress_eventbox, False, False )
634 # make the button box
635 buttonbox = gtk.HBox()
637 # A wrapper to help create DualActionButtons with the right settings
638 create_da = lambda a, b, c=None, d=None: widgets.DualActionButton(
639 a, b, c, d, settings.dual_action_button_delay,
640 settings.enable_dual_action_btn )
642 self.rrewind_button = create_da(
643 generate_image('media-skip-backward.png'),
644 lambda: self.do_seek(-1*settings.seek_long),
645 generate_image(gtk.STOCK_GOTO_FIRST, True),
646 player.playlist.prev)
647 buttonbox.add(self.rrewind_button)
649 self.rewind_button = create_da(
650 generate_image('media-seek-backward.png'),
651 lambda: self.do_seek(-1*settings.seek_short))
652 buttonbox.add(self.rewind_button)
654 self.play_pause_button = gtk.Button('')
655 image(self.play_pause_button, 'media-playback-start.png')
656 self.play_pause_button.connect( 'clicked',
657 self.on_btn_play_pause_clicked )
658 self.play_pause_button.set_sensitive(False)
659 buttonbox.add(self.play_pause_button)
661 self.forward_button = create_da(
662 generate_image('media-seek-forward.png'),
663 lambda: self.do_seek(settings.seek_short))
664 buttonbox.add(self.forward_button)
666 self.fforward_button = create_da(
667 generate_image('media-skip-forward.png'),
668 lambda: self.do_seek(settings.seek_long),
669 generate_image(gtk.STOCK_GOTO_LAST, True),
670 player.playlist.next)
671 buttonbox.add(self.fforward_button)
673 self.bookmarks_button = create_da(
674 generate_image('bookmark-new.png'),
675 player.add_bookmark_at_current_position,
676 generate_image(gtk.STOCK_JUMP_TO, True),
677 lambda *args: self.notify('select-current-item-request'))
678 buttonbox.add(self.bookmarks_button)
679 self.set_controls_sensitivity(False)
680 main_vbox.pack_start(buttonbox, False, False)
682 if platform.MAEMO:
683 self.__gui_root.main_window.connect( 'key-press-event',
684 self.on_key_press )
686 # Disable focus for all widgets, so we can use the cursor
687 # keys + enter to directly control our media player, which
688 # is handled by "key-press-event"
689 for w in (
690 self.rrewind_button, self.rewind_button,
691 self.play_pause_button, self.forward_button,
692 self.fforward_button, self.progress,
693 self.bookmarks_button, ):
694 w.unset_flags(gtk.CAN_FOCUS)
696 def set_controls_sensitivity(self, sensitive):
697 for button in self.forward_button, self.rewind_button, \
698 self.fforward_button, self.rrewind_button:
700 button.set_sensitive(sensitive)
702 # the play/pause button should always be available except
703 # for when the player starts without a file
704 self.play_pause_button.set_sensitive(True)
706 def on_dual_action_setting_changed( self, *args ):
707 for button in self.forward_button, self.rewind_button, \
708 self.fforward_button, self.rrewind_button, \
709 self.bookmarks_button:
711 button.set_longpress_enabled( settings.enable_dual_action_btn )
712 button.set_duration( settings.dual_action_button_delay )
714 def on_key_press(self, widget, event):
715 if platform.MAEMO:
716 if event.keyval == gtk.keysyms.Left: # seek back
717 self.do_seek( -1 * settings.seek_long )
718 elif event.keyval == gtk.keysyms.Right: # seek forward
719 self.do_seek( settings.seek_long )
720 elif event.keyval == gtk.keysyms.Return: # play/pause
721 self.on_btn_play_pause_clicked()
723 def on_player_stopped(self):
724 self.stop_progress_timer()
725 self.set_controls_sensitivity(False)
726 image(self.play_pause_button, 'media-playback-start.png')
728 def on_player_playing(self):
729 self.start_progress_timer()
730 image(self.play_pause_button, 'media-playback-pause.png')
731 self.set_controls_sensitivity(True)
733 def on_player_new_track(self):
734 for widget in [self.title_label,self.artist_label,self.album_label]:
735 widget.set_markup('')
736 widget.hide()
738 self.cover_art.hide()
739 self.has_coverart = False
741 def on_player_new_metadata(self):
742 metadata = player.playlist.get_file_metadata()
743 self.set_metadata(metadata)
745 if not player.playing:
746 position = player.playlist.get_current_position()
747 estimated_length = metadata.get('length', 0)
748 self.set_progress_callback( position, estimated_length )
750 def on_player_paused( self, position, duration ):
751 self.stop_progress_timer() # This should save some power
752 self.set_progress_callback( position, duration )
753 image(self.play_pause_button, 'media-playback-start.png')
755 def on_player_end_of_playlist(self, loop):
756 pass
758 def reset_progress(self):
759 self.progress.set_fraction(0)
760 self.set_progress_callback(0,0)
762 def set_progress_callback(self, time_elapsed, total_time):
763 """ times must be in nanoseconds """
764 time_string = "%s / %s" % ( util.convert_ns(time_elapsed),
765 util.convert_ns(total_time) )
766 self.progress.set_text( time_string )
767 fraction = float(time_elapsed) / float(total_time) if total_time else 0
768 self.progress.set_fraction( fraction )
770 def on_progressbar_changed(self, widget, event):
771 if ( not settings.progress_locked and
772 event.type == gtk.gdk.BUTTON_PRESS and event.button == 1 ):
773 new_fraction = event.x/float(widget.get_allocation().width)
774 resp = player.do_seek(percent=new_fraction)
775 if resp:
776 # Preemptively update the progressbar to make seeking smoother
777 self.set_progress_callback( *resp )
779 def on_btn_play_pause_clicked(self, widget=None):
780 player.play_pause_toggle()
782 def progress_timer_callback( self ):
783 if player.playing and not player.seeking:
784 pos_int, dur_int = player.get_position_duration()
785 # This prevents bogus values from being set while seeking
786 if ( pos_int > 10**9 ) and ( dur_int > 10**9 ):
787 self.set_progress_callback( pos_int, dur_int )
788 return True
790 def start_progress_timer( self ):
791 if self.progress_timer_id is not None:
792 self.stop_progress_timer()
794 self.progress_timer_id = gobject.timeout_add(
795 1000, self.progress_timer_callback )
797 def stop_progress_timer( self ):
798 if self.progress_timer_id is not None:
799 gobject.source_remove( self.progress_timer_id )
800 self.progress_timer_id = None
802 def get_coverart_size( self ):
803 if platform.MAEMO:
804 if self.__gui_root.fullscreen:
805 size = coverart_sizes['maemo fullscreen']
806 else:
807 size = coverart_sizes['maemo']
808 else:
809 size = coverart_sizes['normal']
811 return size, size
813 def set_coverart( self, pixbuf ):
814 self.cover_art.set_from_pixbuf(pixbuf)
815 self.cover_art.show()
816 self.has_coverart = True
818 def set_metadata( self, tag_message ):
819 tags = { 'title': self.title_label, 'artist': self.artist_label,
820 'album': self.album_label }
822 # set the coverart
823 if tag_message.has_key('image') and tag_message['image'] is not None:
824 value = tag_message['image']
826 pbl = gtk.gdk.PixbufLoader()
827 try:
828 pbl.write(value)
829 pbl.close()
831 x, y = self.get_coverart_size()
832 pixbuf = pbl.get_pixbuf()
833 pixbuf = pixbuf.scale_simple( x, y, gtk.gdk.INTERP_BILINEAR )
834 self.set_coverart(pixbuf)
835 except Exception, e:
836 self.__log.exception('Error setting coverart...')
838 # set the text metadata
839 for tag,value in tag_message.iteritems():
840 if tags.has_key(tag) and value is not None and value.strip():
841 tags[tag].set_markup('<big>'+value+'</big>')
842 tags[tag].set_alignment( 0.5*int(not self.has_coverart), 0.5)
843 tags[tag].show()
845 if tag == 'title':
846 # make the title bold
847 tags[tag].set_markup('<b><big>'+value+'</big></b>')
849 if not platform.MAEMO:
850 value += ' - Panucci'
852 self.__gui_root.main_window.set_title( value )
854 def do_seek(self, seek_amount):
855 resp = player.do_seek(from_current=seek_amount*10**9)
856 if resp:
857 # Preemptively update the progressbar to make seeking smoother
858 self.set_progress_callback( *resp )
862 ##################################################
863 # PlaylistTab
864 ##################################################
865 class PlaylistTab(gtk.VBox):
866 def __init__(self, main_window):
867 gtk.VBox.__init__(self)
868 self.__log = logging.getLogger('panucci.panucci.BookmarksWindow')
869 self.main = main_window
871 self.__model = gtk.TreeStore(
872 # uid, name, position
873 gobject.TYPE_STRING, gobject.TYPE_STRING, gobject.TYPE_STRING )
875 self.set_spacing(5)
876 self.treeview = gtk.TreeView()
877 self.treeview.set_model(self.__model)
878 self.treeview.set_headers_visible(True)
879 tree_selection = self.treeview.get_selection()
880 # This breaks drag and drop, only use single selection for now
881 # tree_selection.set_mode(gtk.SELECTION_MULTIPLE)
882 tree_selection.connect('changed', self.tree_selection_changed)
884 # The tree lines look nasty on maemo
885 if platform.DESKTOP:
886 self.treeview.set_enable_tree_lines(True)
887 self.update_model()
889 ncol = gtk.TreeViewColumn(_('Name'))
890 ncell = gtk.CellRendererText()
891 ncell.set_property('ellipsize', pango.ELLIPSIZE_END)
892 ncell.set_property('editable', True)
893 ncell.connect('edited', self.label_edited)
894 ncol.set_expand(True)
895 ncol.pack_start(ncell)
896 ncol.add_attribute(ncell, 'text', 1)
898 tcol = gtk.TreeViewColumn(_('Position'))
899 tcell = gtk.CellRendererText()
900 tcol.pack_start(tcell)
901 tcol.add_attribute(tcell, 'text', 2)
903 self.treeview.append_column(ncol)
904 self.treeview.append_column(tcol)
905 self.treeview.connect('drag-data-received', self.drag_data_recieved)
906 self.treeview.connect('drag_data_get', self.drag_data_get_data)
908 treeview_targets = [
909 ( 'playlist_row_data', gtk.TARGET_SAME_WIDGET, 0 ) ]
911 self.treeview.enable_model_drag_source(
912 gtk.gdk.BUTTON1_MASK, treeview_targets, gtk.gdk.ACTION_COPY )
914 self.treeview.enable_model_drag_dest(
915 treeview_targets, gtk.gdk.ACTION_COPY )
917 sw = gtk.ScrolledWindow()
918 sw.set_policy(gtk.POLICY_AUTOMATIC, gtk.POLICY_AUTOMATIC)
919 sw.set_shadow_type(gtk.SHADOW_IN)
920 sw.add(self.treeview)
921 self.add(sw)
923 self.hbox = gtk.HBox()
925 self.add_button = gtk.Button(gtk.STOCK_NEW)
926 self.add_button.set_use_stock(True)
927 set_stock_button_text( self.add_button, _('Add File') )
928 self.add_button.connect('clicked', self.add_file)
929 self.hbox.pack_start(self.add_button, True, True)
931 self.dir_button = gtk.Button(gtk.STOCK_OPEN)
932 self.dir_button.set_use_stock(True)
933 set_stock_button_text( self.dir_button, _('Add Directory') )
934 self.dir_button.connect('clicked', self.add_directory)
935 self.hbox.pack_start(self.dir_button, True, True)
937 self.remove_button = widgets.DualActionButton(
938 generate_image(gtk.STOCK_REMOVE, True),
939 self.remove_bookmark,
940 generate_image(gtk.STOCK_CANCEL, True),
941 lambda *a: player.playlist.reset_playlist() )
942 #self.remove_button.set_use_stock(True)
943 #self.remove_button.connect('clicked', self.remove_bookmark)
944 self.hbox.pack_start(self.remove_button, True, True)
946 self.jump_button = gtk.Button(gtk.STOCK_JUMP_TO)
947 self.jump_button.set_use_stock(True)
948 self.jump_button.connect('clicked', self.jump_bookmark)
949 self.hbox.pack_start(self.jump_button, True, True)
951 self.info_button = gtk.Button()
952 self.info_button.add(
953 gtk.image_new_from_stock(gtk.STOCK_INFO, gtk.ICON_SIZE_BUTTON))
954 self.info_button.connect('clicked', self.show_playlist_item_details)
955 self.hbox.pack_start(self.info_button, True, True)
957 self.pack_start(self.hbox, False, True)
959 player.playlist.register( 'file_queued',
960 lambda x,y,z: self.update_model() )
961 player.playlist.register( 'bookmark_added', self.on_bookmark_added )
963 self.show_all()
965 def tree_selection_changed(self, treeselection):
966 count = treeselection.count_selected_rows()
967 self.remove_button.set_sensitive(count > 0)
968 self.jump_button.set_sensitive(count == 1)
969 self.info_button.set_sensitive(count == 1)
971 def drag_data_get_data(
972 self, treeview, context, selection, target_id, timestamp):
974 treeselection = treeview.get_selection()
975 model, iter = treeselection.get_selected()
976 # only allow moving around top-level parents
977 if model.iter_parent(iter) is None:
978 # send the path of the selected row
979 data = model.get_string_from_iter(iter)
980 selection.set(selection.target, 8, data)
981 else:
982 self.__log.debug("Can't move children...")
984 def drag_data_recieved(
985 self, treeview, context, x, y, selection, info, timestamp):
987 drop_info = treeview.get_dest_row_at_pos(x, y)
989 # TODO: If user drags the row past the last row, drop_info is None
990 # I'm not sure if it's safe to simply assume that None is
991 # euqivalent to the last row...
992 if None not in [ drop_info and selection.data ]:
993 model = treeview.get_model()
994 path, position = drop_info
996 from_iter = model.get_iter_from_string(selection.data)
998 # make sure the to_iter doesn't have a parent
999 to_iter = model.get_iter(path)
1000 if model.iter_parent(to_iter) is not None:
1001 to_iter = model.iter_parent(to_iter)
1003 from_row = model.get_path(from_iter)[0]
1004 to_row = path[0]
1006 if ( position == gtk.TREE_VIEW_DROP_BEFORE or
1007 position == gtk.TREE_VIEW_DROP_INTO_OR_BEFORE ):
1008 model.move_before( from_iter, to_iter )
1009 to_row = to_row - 1 if from_row < to_row else to_row
1010 elif ( position == gtk.TREE_VIEW_DROP_AFTER or
1011 position == gtk.TREE_VIEW_DROP_INTO_OR_AFTER ):
1012 model.move_after( from_iter, to_iter )
1013 to_row = to_row + 1 if from_row > to_row else to_row
1014 else:
1015 self.__log.debug('Drop not supported: %s', position)
1017 # don't do anything if we're not actually moving rows around
1018 if from_row != to_row:
1019 player.playlist.move_item( from_row, to_row )
1021 else:
1022 self.__log.debug('No drop_data or selection.data available')
1024 def update_model(self):
1025 plist = player.playlist
1026 path_info = self.treeview.get_path_at_pos(0,0)
1027 path = path_info[0] if path_info is not None else None
1029 self.__model.clear()
1031 # build the tree
1032 for item, data in plist.get_playlist_item_ids():
1033 parent = self.__model.append(None, (item, data.get('title'), None))
1035 for bid, bname, bpos in plist.get_bookmarks_from_item_id( item ):
1036 nice_bpos = util.convert_ns(bpos)
1037 self.__model.append( parent, (bid, bname, nice_bpos) )
1039 self.treeview.expand_all()
1041 if path is not None:
1042 self.treeview.scroll_to_cell(path)
1044 def label_edited(self, cellrenderer, path, new_text):
1045 iter = self.__model.get_iter(path)
1046 old_text = self.__model.get_value(iter, 1)
1048 if new_text.strip() and old_text != new_text:
1049 # this loop will only run once, because only one cell can be
1050 # edited at a time, we use it to get the item and bookmark ids
1051 for m, bkmk_id, biter, item_id, iiter in self.__cur_selection():
1052 self.__model.set_value(iter, 1, new_text)
1053 player.playlist.update_bookmark(
1054 item_id, bkmk_id, name=new_text )
1055 else:
1056 self.__model.set_value(iter, 1, old_text)
1058 def on_bookmark_added(self, parent_id, bookmark_name, position):
1059 self.main.notify(_('Bookmark added: %s') % bookmark_name)
1060 self.update_model()
1062 def add_file(self, widget):
1063 filename = get_file_from_filechooser(self.main.main_window)
1064 if filename is not None:
1065 player.playlist.append(filename)
1067 def add_directory(self, widget):
1068 directory = get_file_from_filechooser(
1069 self.main.main_window, folder=True )
1070 if directory is not None:
1071 player.playlist.load_directory(directory, append=True)
1073 def __cur_selection(self):
1074 selection = self.treeview.get_selection()
1075 model, bookmark_paths = selection.get_selected_rows()
1077 # Convert the paths to gtk.TreeRowReference objects, because we
1078 # might modify the model while this generator is running
1079 bookmark_refs = [gtk.TreeRowReference(model, p) for p in bookmark_paths]
1081 for reference in bookmark_refs:
1082 bookmark_iter = model.get_iter(reference.get_path())
1083 item_iter = model.iter_parent(bookmark_iter)
1085 # bookmark_iter is actually an item_iter
1086 if item_iter is None:
1087 item_iter = bookmark_iter
1088 item_id = model.get_value(item_iter, 0)
1089 bookmark_id, bookmark_iter = None, None
1090 else:
1091 bookmark_id = model.get_value(bookmark_iter, 0)
1092 item_id = model.get_value(item_iter, 0)
1094 yield model, bookmark_id, bookmark_iter, item_id, item_iter
1096 def remove_bookmark(self, w=None):
1097 for model, bkmk_id, bkmk_iter, item_id, item_iter in self.__cur_selection():
1098 player.playlist.remove_bookmark( item_id, bkmk_id )
1099 if bkmk_iter is not None:
1100 model.remove(bkmk_iter)
1101 elif item_iter is not None:
1102 model.remove(item_iter)
1104 def select_current_item(self):
1105 model = self.treeview.get_model()
1106 selection = self.treeview.get_selection()
1107 current_item_id = str(player.playlist.get_current_item())
1108 for row in iter(model):
1109 if model.get_value(row.iter, 0) == current_item_id:
1110 selection.unselect_all()
1111 self.treeview.set_cursor(row.path)
1112 self.treeview.scroll_to_cell(row.path, use_align=True)
1113 break
1115 def show_playlist_item_details(self, w):
1116 selection = self.treeview.get_selection()
1117 if selection.count_selected_rows() == 1:
1118 selected = self.__cur_selection().next()
1119 model, bkmk_id, bkmk_iter, item_id, item_iter = selected
1120 playlist_item = player.playlist.get_item_by_id(item_id)
1121 PlaylistItemDetails(self.main, playlist_item)
1123 def jump_bookmark(self, w):
1124 selected = list(self.__cur_selection())
1125 if len(selected) == 1:
1126 # It should be guranteed by the fact that we only enable the
1127 # "Jump to" button when the selection count equals 1.
1128 model, bkmk_id, bkmk_iter, item_id, item_iter = selected.pop(0)
1129 player.playlist.load_from_bookmark_id(item_id, bkmk_id)
1131 # FIXME: The player/playlist should be able to take care of this
1132 if not player.playing:
1133 player.play()
1136 ##################################################
1137 # PlaylistItemDetails
1138 ##################################################
1139 class PlaylistItemDetails(gtk.Dialog):
1140 def __init__(self, main, playlist_item):
1141 gtk.Dialog.__init__( self, _('Playlist item details'),
1142 main.main_window, gtk.DIALOG_MODAL,
1143 (gtk.STOCK_CLOSE, gtk.RESPONSE_OK))
1145 self.main = main
1146 self.fill(playlist_item)
1147 self.set_has_separator(False)
1148 self.set_resizable(False)
1149 self.show_all()
1150 self.run()
1151 self.destroy()
1153 def fill(self, playlist_item):
1154 t = gtk.Table(10, 2)
1155 self.vbox.pack_start(t, expand=False)
1157 metadata = playlist_item.metadata
1159 t.attach(gtk.Label(_('Custom title:')), 0, 1, 0, 1)
1160 t.attach(gtk.Label(_('ID:')), 0, 1, 1, 2)
1161 t.attach(gtk.Label(_('Playlist ID:')), 0, 1, 2, 3)
1162 t.attach(gtk.Label(_('Filepath:')), 0, 1, 3, 4)
1164 row_num = 4
1165 for key in metadata:
1166 if metadata[key] is not None:
1167 t.attach( gtk.Label(key.capitalize()+':'),
1168 0, 1, row_num, row_num+1 )
1169 row_num += 1
1171 t.foreach(lambda x, y: x.set_alignment(1, 0.5), None)
1172 t.foreach(lambda x, y: x.set_markup('<b>%s</b>' % x.get_label()), None)
1174 t.attach(gtk.Label(playlist_item.title or _('<not modified>')),1,2,0,1)
1175 t.attach(gtk.Label(str(playlist_item)), 1, 2, 1, 2)
1176 t.attach(gtk.Label(playlist_item.playlist_id), 1, 2, 2, 3)
1177 t.attach(gtk.Label(playlist_item.filepath), 1, 2, 3, 4)
1179 row_num = 4
1180 for key in metadata:
1181 value = metadata[key]
1182 if key == 'length':
1183 value = util.convert_ns(value)
1184 if metadata[key] is not None:
1185 t.attach( gtk.Label( str(value) or _('<not set>')),
1186 1, 2, row_num, row_num+1)
1187 row_num += 1
1189 t.foreach(lambda x, y: x.get_alignment() == (0.5, 0.5) and \
1190 x.set_alignment(0, 0.5), None)
1192 t.set_border_width(8)
1193 t.set_row_spacings(4)
1194 t.set_col_spacings(8)
1196 l = gtk.ListStore(str, str)
1197 t = gtk.TreeView(l)
1198 cr = gtk.CellRendererText()
1199 cr.set_property('ellipsize', pango.ELLIPSIZE_END)
1200 c = gtk.TreeViewColumn(_('Title'), cr, text=0)
1201 c.set_expand(True)
1202 t.append_column(c)
1203 c = gtk.TreeViewColumn(_('Time'), gtk.CellRendererText(), text=1)
1204 t.append_column(c)
1205 playlist_item.load_bookmarks()
1206 for bookmark in playlist_item.bookmarks:
1207 l.append([bookmark.bookmark_name, \
1208 util.convert_ns(bookmark.seek_position)])
1210 sw = gtk.ScrolledWindow()
1211 sw.set_shadow_type(gtk.SHADOW_IN)
1212 sw.add(t)
1213 sw.set_policy(gtk.POLICY_AUTOMATIC, gtk.POLICY_AUTOMATIC)
1214 e = gtk.Expander(_('Bookmarks'))
1215 e.add(sw)
1216 self.vbox.pack_start(e)
1219 def run(filename=None):
1220 PanucciGUI(filename)
1221 gtk.main()
1223 if __name__ == '__main__':
1224 log.error( 'Use the "panucci" executable to run this program.' )
1225 log.error( 'Exiting...' )
1226 sys.exit(1)