Immediately hide main window on close
[panucci.git] / src / panucci / main.py
blob26f9e473132ff9760d3107387622ae96ab437b49
1 #!/usr/bin/env python
2 # -*- coding: utf-8 -*-
4 # This file is part of Panucci.
5 # Copyright (c) 2008-2010 The Panucci Audiobook and Podcast Player Project
7 # Panucci is free software: you can redistribute it and/or modify
8 # it under the terms of the GNU General Public License as published by
9 # the Free Software Foundation, either version 3 of the License, or
10 # (at your option) any later version.
12 # Panucci is distributed in the hope that it will be useful,
13 # but WITHOUT ANY WARRANTY; without even the implied warranty of
14 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
15 # GNU General Public License for more details.
17 # You should have received a copy of the GNU General Public License
18 # along with Panucci. If not, see <http://www.gnu.org/licenses/>.
20 # Based on http://thpinfo.com/2008/panucci/:
21 # A resuming media player for Podcasts and Audiobooks
22 # Copyright (c) 2008-05-26 Thomas Perl <thpinfo.com>
23 # (based on http://pygstdocs.berlios.de/pygst-tutorial/seeking.html)
26 from __future__ import absolute_import
29 import logging
30 import sys
31 import os, os.path
32 import time
34 import gtk
35 import gobject
36 import pango
38 import cgi
40 import panucci
42 from panucci import widgets
43 from panucci import util
44 from panucci import platform
46 log = logging.getLogger('panucci.panucci')
48 try:
49 import pynotify
50 pynotify.init('Panucci')
51 have_pynotify = True
52 except:
53 have_pynotify = False
55 try:
56 import hildon
57 except:
58 if platform.MAEMO:
59 log.critical( 'Using GTK widgets, install "python2.5-hildon" '
60 'for this to work properly.' )
62 from panucci.settings import settings
63 from panucci.player import player
64 from panucci.dbusinterface import interface
65 from panucci.services import ObservableService
67 about_name = 'Panucci'
68 about_text = _('Resuming audiobook and podcast player')
69 about_authors = ['Thomas Perl', 'Nick (nikosapi)', 'Matthew Taylor']
70 about_website = 'http://gpodder.org/panucci/'
71 about_bugtracker = 'http://bugs.maemo.org/enter_bug.cgi?product=Panucci'
72 about_donate = 'http://gpodder.org/donate'
73 about_copyright = '© 2008-2010 Thomas Perl and the Panucci Team'
75 coverart_sizes = {
76 'normal' : 110,
77 'maemo' : 200,
78 'maemo fullscreen' : 275,
81 gtk.icon_size_register('panucci-button', 32, 32)
83 def find_image(filename):
84 bin_dir = os.path.dirname(sys.argv[0])
85 locations = [
86 os.path.join(bin_dir, '..', 'share', 'panucci'),
87 os.path.join(bin_dir, '..', 'icons'),
88 '/opt/panucci',
91 for location in locations:
92 fn = os.path.abspath(os.path.join(location, filename))
93 if os.path.exists(fn):
94 return fn
96 def generate_image(filename, is_stock=False):
97 image = None
98 if is_stock:
99 image = gtk.image_new_from_stock(
100 filename, gtk.icon_size_from_name('panucci-button') )
101 else:
102 filename = find_image(filename)
103 if filename is not None:
104 image = gtk.image_new_from_file(filename)
105 if image is not None:
106 if platform.MAEMO:
107 image.set_padding(20, 20)
108 else:
109 image.set_padding(5, 5)
110 image.show()
111 return image
113 def image(widget, filename, is_stock=False):
114 child = widget.get_child()
115 if child is not None:
116 widget.remove(child)
117 image = generate_image(filename, is_stock)
118 if image is not None:
119 widget.add(image)
121 def dialog( toplevel_window, title, question, description,
122 affirmative_button=gtk.STOCK_YES, negative_button=gtk.STOCK_NO,
123 abortion_button=gtk.STOCK_CANCEL ):
125 """Present the user with a yes/no/cancel dialog.
126 The return value is either True, False or None, depending on which
127 button has been pressed in the dialog:
129 affirmative button (default: Yes) => True
130 negative button (defaut: No) => False
131 abortion button (default: Cancel) => None
133 When the dialog is closed with the "X" button in the window manager
134 decoration, the return value is always None (same as abortion button).
136 You can set any of the affirmative_button, negative_button or
137 abortion_button values to "None" to hide the corresponding action.
139 dlg = gtk.MessageDialog( toplevel_window, gtk.DIALOG_MODAL,
140 gtk.MESSAGE_QUESTION, message_format=question )
142 dlg.set_title(title)
144 if abortion_button is not None:
145 dlg.add_button(abortion_button, gtk.RESPONSE_CANCEL)
146 if negative_button is not None:
147 dlg.add_button(negative_button, gtk.RESPONSE_NO)
148 if affirmative_button is not None:
149 dlg.add_button(affirmative_button, gtk.RESPONSE_YES)
151 dlg.format_secondary_text(description)
153 response = dlg.run()
154 dlg.destroy()
156 if response == gtk.RESPONSE_YES:
157 return True
158 elif response == gtk.RESPONSE_NO:
159 return False
160 elif response in [gtk.RESPONSE_CANCEL, gtk.RESPONSE_DELETE_EVENT]:
161 return None
163 def get_file_from_filechooser(
164 toplevel_window, folder=False, save_file=False, save_to=None):
166 if folder:
167 open_action = gtk.FILE_CHOOSER_ACTION_SELECT_FOLDER
168 else:
169 open_action = gtk.FILE_CHOOSER_ACTION_OPEN
171 if platform.FREMANTLE:
172 if save_file:
173 dlg = gobject.new(hildon.FileChooserDialog, \
174 action=gtk.FILE_CHOOSER_ACTION_SAVE)
175 else:
176 dlg = gobject.new(hildon.FileChooserDialog, \
177 action=open_action)
178 elif platform.MAEMO:
179 if save_file:
180 args = ( toplevel_window, gtk.FILE_CHOOSER_ACTION_SAVE )
181 else:
182 args = ( toplevel_window, open_action )
184 dlg = hildon.FileChooserDialog( *args )
185 else:
186 if save_file:
187 args = ( _('Select file to save playlist to'), None,
188 gtk.FILE_CHOOSER_ACTION_SAVE,
189 (( gtk.STOCK_CANCEL, gtk.RESPONSE_REJECT,
190 gtk.STOCK_SAVE, gtk.RESPONSE_OK )) )
191 else:
192 args = ( _('Select podcast or audiobook'), None, open_action,
193 (( gtk.STOCK_CANCEL, gtk.RESPONSE_REJECT,
194 gtk.STOCK_OPEN, gtk.RESPONSE_OK )) )
196 dlg = gtk.FileChooserDialog(*args)
198 current_folder = os.path.expanduser(settings.last_folder)
200 if current_folder is not None and os.path.isdir(current_folder):
201 dlg.set_current_folder(current_folder)
203 if save_file and save_to is not None:
204 dlg.set_current_name(save_to)
206 if dlg.run() == gtk.RESPONSE_OK:
207 filename = dlg.get_filename()
208 settings.last_folder = dlg.get_current_folder()
209 else:
210 filename = None
212 dlg.destroy()
213 return filename
215 def set_stock_button_text( button, text ):
216 alignment = button.get_child()
217 hbox = alignment.get_child()
218 image, label = hbox.get_children()
219 label.set_text(text)
221 ##################################################
222 # PanucciGUI
223 ##################################################
224 class PanucciGUI(object):
225 """ The object that holds the entire panucci gui """
227 def __init__(self, filename=None):
228 self.__log = logging.getLogger('panucci.panucci.PanucciGUI')
229 interface.register_gui(self)
231 # Build the base ui (window and menubar)
232 if platform.MAEMO:
233 self.app = hildon.Program()
234 if platform.FREMANTLE:
235 window = hildon.StackableWindow()
236 else:
237 window = hildon.Window()
238 self.app.add_window(window)
239 else:
240 window = gtk.Window(gtk.WINDOW_TOPLEVEL)
242 self.main_window = window
243 window.set_title('Panucci')
244 self.window_icon = find_image('panucci.png')
245 if self.window_icon is not None:
246 window.set_icon_from_file( self.window_icon )
247 window.set_default_size(400, -1)
248 window.set_border_width(0)
249 window.connect("destroy", self.destroy)
251 # Add the tabs (they are private to prevent us from trying to do
252 # something like gui_root.player_tab.some_function() from inside
253 # playlist_tab or vice-versa)
254 self.__player_tab = PlayerTab(self)
255 self.__playlist_tab = PlaylistTab(self)
257 if platform.FREMANTLE:
258 self.playlist_window = hildon.StackableWindow()
259 else:
260 self.playlist_window = gtk.Window(gtk.WINDOW_TOPLEVEL)
261 self.playlist_window.connect('delete-event', gtk.Widget.hide_on_delete)
262 self.playlist_window.set_title(_('Playlist'))
263 self.playlist_window.set_transient_for(self.main_window)
264 self.playlist_window.add(self.__playlist_tab)
266 self.create_actions()
268 if platform.MAEMO:
269 if platform.FREMANTLE:
270 window.set_app_menu(self.create_app_menu())
271 else:
272 window.set_menu(self.create_menu())
273 window.add(self.__player_tab)
274 else:
275 menu_vbox = gtk.VBox()
276 menu_vbox.set_spacing(0)
277 window.add(menu_vbox)
278 menu_bar = gtk.MenuBar()
279 self.create_desktop_menu(menu_bar)
280 menu_vbox.pack_start(menu_bar, False, False, 0)
281 menu_bar.show()
282 menu_vbox.pack_end(self.__player_tab, True, True, 6)
284 # Tie it all together!
285 self.__ignore_queue_check = False
286 self.__window_fullscreen = False
288 if platform.MAEMO and interface.headset_device is not None:
289 # Enable play/pause with headset button
290 interface.headset_device.connect_to_signal(
291 'Condition', self.handle_headset_button )
293 self.main_window.connect('key-press-event', self.on_key_press)
294 player.playlist.register( 'file_queued', self.on_file_queued )
296 player.playlist.register( 'playlist-to-be-overwritten',
297 self.check_queue )
298 self.__player_tab.register( 'select-current-item-request',
299 self.__select_current_item )
301 self.main_window.show_all()
303 # this should be done when the gui is ready
304 player.init(filepath=filename)
306 pos_int, dur_int = player.get_position_duration()
307 # This prevents bogus values from being set while seeking
308 if (pos_int > 10**9) and (dur_int > 10**9):
309 self.set_progress_callback(pos_int, dur_int)
311 def create_actions(self):
312 self.action_open = gtk.Action('open', _('Open'), _('Open a file or playlist'), gtk.STOCK_OPEN)
313 self.action_open.connect('activate', self.open_file_callback)
314 self.action_save = gtk.Action('save', _('Save playlist'), _('Save current playlist to file'), gtk.STOCK_SAVE_AS)
315 self.action_save.connect('activate', self.save_to_playlist_callback)
316 self.action_playlist = gtk.Action('playlist', _('Playlist'), _('Open the current playlist'), None)
317 self.action_playlist.connect('activate', lambda a: self.playlist_window.show())
318 self.action_about = gtk.Action('about', _('About Panucci'), _('Show application version'), gtk.STOCK_ABOUT)
319 self.action_about.connect('activate', self.about_callback)
320 self.action_quit = gtk.Action('quit', _('Quit'), _('Close Panucci'), gtk.STOCK_QUIT)
321 self.action_quit.connect('activate', self.destroy)
323 def create_desktop_menu(self, menu_bar):
324 file_menu_item = gtk.MenuItem(_('File'))
325 file_menu = gtk.Menu()
326 file_menu.append(self.action_open.create_menu_item())
327 file_menu.append(self.action_save.create_menu_item())
328 file_menu.append(gtk.SeparatorMenuItem())
329 file_menu.append(self.action_quit.create_menu_item())
330 file_menu_item.set_submenu(file_menu)
331 menu_bar.append(file_menu_item)
333 tools_menu_item = gtk.MenuItem(_('Tools'))
334 tools_menu = gtk.Menu()
335 tools_menu.append(self.action_playlist.create_menu_item())
336 tools_menu_item.set_submenu(tools_menu)
337 menu_bar.append(tools_menu_item)
339 help_menu_item = gtk.MenuItem(_('Help'))
340 help_menu = gtk.Menu()
341 help_menu.append(self.action_about.create_menu_item())
342 help_menu_item.set_submenu(help_menu)
343 menu_bar.append(help_menu_item)
345 def create_app_menu(self):
346 menu = hildon.AppMenu()
348 b = gtk.Button(_('Playlist'))
349 b.connect('clicked', lambda b: self.__player_tab.notify('select-current-item-request'))
350 menu.append(b)
352 b = gtk.Button(_('About'))
353 b.connect('clicked', self.about_callback)
354 menu.append(b)
356 menu.show_all()
357 return menu
359 def create_menu(self):
360 # the main menu
361 menu = gtk.Menu()
363 menu_open = gtk.ImageMenuItem(_('Open playlist'))
364 menu_open.set_image(
365 gtk.image_new_from_stock(gtk.STOCK_OPEN, gtk.ICON_SIZE_MENU))
366 menu_open.connect("activate", self.open_file_callback)
367 menu.append(menu_open)
369 # the recent files menu
370 self.menu_recent = gtk.MenuItem(_('Open recent playlist'))
371 menu.append(self.menu_recent)
372 self.create_recent_files_menu()
374 menu.append(gtk.SeparatorMenuItem())
376 menu_save = gtk.ImageMenuItem(_('Save current playlist'))
377 menu_save.set_image(
378 gtk.image_new_from_stock(gtk.STOCK_SAVE_AS, gtk.ICON_SIZE_MENU))
379 menu_save.connect("activate", self.save_to_playlist_callback)
380 menu.append(menu_save)
382 menu.append(gtk.SeparatorMenuItem())
384 # the settings sub-menu
385 menu_settings = gtk.MenuItem(_('Settings'))
386 menu.append(menu_settings)
388 menu_settings_sub = gtk.Menu()
389 menu_settings.set_submenu(menu_settings_sub)
391 menu_settings_enable_dual_action = gtk.CheckMenuItem(
392 _('Enable dual-action buttons') )
393 settings.attach_checkbutton( menu_settings_enable_dual_action,
394 'enable_dual_action_btn' )
395 menu_settings_sub.append(menu_settings_enable_dual_action)
397 menu_settings_lock_progress = gtk.CheckMenuItem(_('Lock Progress Bar'))
398 settings.attach_checkbutton( menu_settings_lock_progress,
399 'progress_locked' )
400 menu_settings_sub.append(menu_settings_lock_progress)
402 menu_about = gtk.ImageMenuItem(gtk.STOCK_ABOUT)
403 menu_about.connect("activate", self.about_callback)
404 menu.append(menu_about)
406 menu.append(gtk.SeparatorMenuItem())
408 menu_quit = gtk.ImageMenuItem(gtk.STOCK_QUIT)
409 menu_quit.connect("activate", self.destroy)
410 menu.append(menu_quit)
412 return menu
414 def create_recent_files_menu( self ):
415 max_files = settings.max_recent_files
416 self.recent_files = player.playlist.get_recent_files(max_files)
417 menu_recent_sub = gtk.Menu()
419 if len(self.recent_files) > 0:
420 for f in self.recent_files:
421 # don't include the temporary playlist in the file list
422 if f == panucci.PLAYLIST_FILE: continue
423 # don't include non-existant files
424 if not os.path.exists( f ): continue
425 filename, extension = os.path.splitext(os.path.basename(f))
426 menu_item = gtk.MenuItem( filename.replace('_', ' '))
427 menu_item.connect('activate', self.on_recent_file_activate, f)
428 menu_recent_sub.append(menu_item)
429 else:
430 menu_item = gtk.MenuItem(_('No recent files available.'))
431 menu_item.set_sensitive(False)
432 menu_recent_sub.append(menu_item)
434 self.menu_recent.set_submenu(menu_recent_sub)
436 def notify(self, message):
437 """ Sends a notification using pynotify, returns message """
438 if platform.DESKTOP and have_pynotify:
439 icon = find_image('panucci_64x64.png')
440 notification = pynotify.Notification(self.main_window.get_title(), message, icon)
441 notification.show()
442 elif platform.FREMANTLE:
443 hildon.hildon_banner_show_information(self.main_window, \
444 '', message)
445 elif platform.MAEMO:
446 # Note: This won't work if we're not in the gtk main loop
447 markup = '<b>%s</b>\n<small>%s</small>' % (self.main_window.get_title(), message)
448 hildon.hildon_banner_show_information_with_markup(self.main_window, None, markup)
450 def destroy(self, widget):
451 widget.hide()
452 player.quit()
453 gtk.main_quit()
455 def set_progress_indicator(self, loading_title=False):
456 if platform.FREMANTLE:
457 if loading_title:
458 self.main_window.set_title(_('Loading...'))
459 hildon.hildon_gtk_window_set_progress_indicator(self.main_window, \
460 True)
461 while gtk.events_pending():
462 gtk.main_iteration(False)
464 def show_main_window(self):
465 self.main_window.present()
467 def check_queue(self):
468 """ Makes sure the queue is saved if it has been modified
469 True means a new file can be opened
470 False means the user does not want to continue """
472 if not self.__ignore_queue_check and player.playlist.queue_modified:
473 response = dialog(
474 self.main_window, _('Save current playlist'),
475 _('Current playlist has been modified'),
476 _('Opening a new file will replace the current playlist. ') +
477 _('Do you want to save it before creating a new one?'),
478 affirmative_button=gtk.STOCK_SAVE,
479 negative_button=_('Discard changes'))
481 self.__log.debug('Response to "Save Queue?": %s', response)
483 if response is None:
484 return False
485 elif response:
486 return self.save_to_playlist_callback()
487 elif not response:
488 return True
489 else:
490 return False
491 else:
492 return True
494 def open_file_callback(self, widget=None):
495 if self.check_queue():
496 # set __ingnore__queue_check because we already did the check
497 self.__ignore_queue_check = True
498 filename = get_file_from_filechooser(self.main_window)
499 if filename is not None:
500 self._play_file(filename)
502 self.__ignore_queue_check = False
504 def save_to_playlist_callback(self, widget=None):
505 filename = get_file_from_filechooser(
506 self.main_window, save_file=True, save_to='playlist.m3u' )
508 if filename is None:
509 return False
511 if os.path.isfile(filename):
512 response = dialog( self.main_window, _('File already exists'),
513 _('File already exists'),
514 _('The file %s already exists. You can choose another name or '
515 'overwrite the existing file.') % os.path.basename(filename),
516 affirmative_button=gtk.STOCK_SAVE,
517 negative_button=_('Rename file'))
519 if response is None:
520 return None
522 elif response:
523 pass
524 elif not response:
525 return self.save_to_playlist_callback()
527 ext = util.detect_filetype(filename)
528 if not player.playlist.save_to_new_playlist(filename, ext):
529 self.notify(_('Error saving playlist...'))
530 return False
532 return True
534 def __get_fullscreen(self):
535 return self.__window_fullscreen
537 def __set_fullscreen(self, value):
538 if value != self.__window_fullscreen:
539 if value:
540 self.main_window.fullscreen()
541 else:
542 self.main_window.unfullscreen()
544 self.__window_fullscreen = value
545 player.playlist.send_metadata()
547 fullscreen = property( __get_fullscreen, __set_fullscreen )
549 def on_key_press(self, widget, event):
550 if platform.MAEMO:
551 if event.keyval == gtk.keysyms.F6:
552 self.fullscreen = not self.fullscreen
554 def on_recent_file_activate(self, widget, filepath):
555 self._play_file(filepath)
557 def on_file_queued(self, filepath, success, notify):
558 if notify:
559 filename = os.path.basename(filepath)
560 if success:
561 self.__log.info(
562 self.notify( '%s added successfully.' % filename ))
563 else:
564 self.__log.error(
565 self.notify( 'Error adding %s to the queue.' % filename))
567 def about_callback(self, widget):
568 if platform.FREMANTLE:
569 from panucci.aboutdialog import HeAboutDialog
571 HeAboutDialog.present(self.main_window,
572 about_name,
573 'panucci',
574 panucci.__version__,
575 about_text,
576 about_copyright,
577 about_website,
578 about_bugtracker,
579 about_donate)
580 else:
581 about = gtk.AboutDialog()
582 about.set_transient_for(self.main_window)
583 about.set_name(about_name)
584 about.set_version(panucci.__version__)
585 about.set_copyright(about_copyright)
586 about.set_comments(about_text)
587 about.set_website(about_website)
588 about.set_authors(about_authors)
589 about.set_translator_credits(_('translator-credits'))
590 about.set_logo_icon_name('panucci')
591 about.run()
592 about.destroy()
594 def _play_file(self, filename, pause_on_load=False):
595 player.playlist.load( os.path.abspath(filename) )
597 if player.playlist.is_empty:
598 return False
600 def handle_headset_button(self, event, button):
601 if event == 'ButtonPressed' and button == 'phone':
602 player.play_pause_toggle()
604 def __select_current_item( self ):
605 # Select the currently playing track in the playlist tab
606 # and switch to it (so we can edit bookmarks, etc.. there)
607 self.__playlist_tab.select_current_item()
608 self.playlist_window.show()
610 ##################################################
611 # PlayerTab
612 ##################################################
613 class PlayerTab(ObservableService, gtk.HBox):
614 """ The tab that holds the player elements """
616 signals = [ 'select-current-item-request', ]
618 def __init__(self, gui_root):
619 self.__log = logging.getLogger('panucci.panucci.PlayerTab')
620 self.__gui_root = gui_root
622 gtk.HBox.__init__(self)
623 ObservableService.__init__(self, self.signals, self.__log)
625 # Timers
626 self.progress_timer_id = None
628 self.recent_files = []
629 self.make_player_tab()
630 self.has_coverart = False
632 #settings.register( 'enable_dual_action_btn_changed',
633 # self.on_dual_action_setting_changed )
634 #settings.register( 'dual_action_button_delay_changed',
635 # self.on_dual_action_setting_changed )
636 #settings.register( 'scrolling_labels_changed', lambda v:
637 # setattr( self.title_label, 'scrolling', v ) )
639 player.register( 'stopped', self.on_player_stopped )
640 player.register( 'playing', self.on_player_playing )
641 player.register( 'paused', self.on_player_paused )
642 player.playlist.register( 'end-of-playlist',
643 self.on_player_end_of_playlist )
644 player.playlist.register( 'new-track-loaded',
645 self.on_player_new_track )
646 player.playlist.register( 'new-metadata-available',
647 self.on_player_new_metadata )
649 def make_player_tab(self):
650 main_vbox = gtk.VBox()
651 main_vbox.set_spacing(6)
652 # add a vbox to self
653 self.pack_start(main_vbox, True, True)
655 # a hbox to hold the cover art and metadata vbox
656 metadata_hbox = gtk.HBox()
657 metadata_hbox.set_spacing(6)
658 main_vbox.pack_start(metadata_hbox, True, False)
660 self.cover_art = gtk.Image()
661 metadata_hbox.pack_start( self.cover_art, False, False )
663 # vbox to hold metadata
664 metadata_vbox = gtk.VBox()
665 metadata_vbox.pack_start(gtk.Image(), True, True)
666 self.artist_label = gtk.Label('')
667 self.artist_label.set_ellipsize(pango.ELLIPSIZE_END)
668 metadata_vbox.pack_start(self.artist_label, False, False)
669 self.album_label = gtk.Label('')
670 if platform.FREMANTLE:
671 hildon.hildon_helper_set_logical_font(self.album_label, 'SmallSystemFont')
672 hildon.hildon_helper_set_logical_color(self.album_label, gtk.RC_FG, gtk.STATE_NORMAL, 'SecondaryTextColor')
673 else:
674 self.album_label.modify_font(pango.FontDescription('normal 8'))
675 self.album_label.set_ellipsize(pango.ELLIPSIZE_END)
676 metadata_vbox.pack_start(self.album_label, False, False)
677 self.title_label = widgets.ScrollingLabel('',
678 update_interval=100,
679 pixel_jump=1,
680 delay_btwn_scrolls=5000,
681 delay_halfway=3000)
682 self.title_label.scrolling = settings.scrolling_labels
683 metadata_vbox.pack_start(self.title_label, False, False)
684 metadata_vbox.pack_start(gtk.Image(), True, True)
685 metadata_hbox.pack_start( metadata_vbox, True, True )
687 progress_eventbox = gtk.EventBox()
688 progress_eventbox.set_events(gtk.gdk.BUTTON_PRESS_MASK)
689 progress_eventbox.connect(
690 'button-press-event', self.on_progressbar_changed )
691 self.progress = gtk.ProgressBar()
692 # make the progress bar more "finger-friendly"
693 if platform.FREMANTLE:
694 self.progress.set_size_request(-1, 100)
695 elif platform.MAEMO:
696 self.progress.set_size_request(-1, 50)
697 progress_eventbox.add(self.progress)
698 main_vbox.pack_start( progress_eventbox, False, False )
700 # make the button box
701 buttonbox = gtk.HBox()
703 # A wrapper to help create DualActionButtons with the right settings
704 def create_da(widget, action, widget2=None, action2=None):
705 if platform.FREMANTLE:
706 widget2 = None
707 action2 = None
709 return widgets.DualActionButton(widget, action, \
710 widget2, action2, \
711 settings.dual_action_button_delay, \
712 settings.enable_dual_action_btn)
714 self.rrewind_button = create_da(
715 generate_image('media-skip-backward.png'),
716 lambda: self.do_seek(-1*settings.seek_long),
717 generate_image(gtk.STOCK_GOTO_FIRST, True),
718 player.playlist.prev)
719 buttonbox.add(self.rrewind_button)
721 self.rewind_button = create_da(
722 generate_image('media-seek-backward.png'),
723 lambda: self.do_seek(-1*settings.seek_short))
724 buttonbox.add(self.rewind_button)
726 self.play_pause_button = gtk.Button('')
727 image(self.play_pause_button, 'media-playback-start.png')
728 self.play_pause_button.connect( 'clicked',
729 self.on_btn_play_pause_clicked )
730 self.play_pause_button.set_sensitive(False)
731 buttonbox.add(self.play_pause_button)
733 self.forward_button = create_da(
734 generate_image('media-seek-forward.png'),
735 lambda: self.do_seek(settings.seek_short))
736 buttonbox.add(self.forward_button)
738 self.fforward_button = create_da(
739 generate_image('media-skip-forward.png'),
740 lambda: self.do_seek(settings.seek_long),
741 generate_image(gtk.STOCK_GOTO_LAST, True),
742 player.playlist.next)
743 buttonbox.add(self.fforward_button)
745 self.bookmarks_button = create_da(
746 generate_image('bookmark-new.png'),
747 player.add_bookmark_at_current_position,
748 generate_image(gtk.STOCK_JUMP_TO, True),
749 lambda *args: self.notify('select-current-item-request'))
750 buttonbox.add(self.bookmarks_button)
751 self.set_controls_sensitivity(False)
753 if platform.FREMANTLE:
754 for child in buttonbox.get_children():
755 if isinstance(child, gtk.Button):
756 child.set_name('HildonButton-thumb')
757 buttonbox.set_size_request(-1, 105)
759 main_vbox.pack_start(buttonbox, False, False)
761 if platform.MAEMO:
762 self.__gui_root.main_window.connect( 'key-press-event',
763 self.on_key_press )
765 # Disable focus for all widgets, so we can use the cursor
766 # keys + enter to directly control our media player, which
767 # is handled by "key-press-event"
768 for w in (
769 self.rrewind_button, self.rewind_button,
770 self.play_pause_button, self.forward_button,
771 self.fforward_button, self.progress,
772 self.bookmarks_button, ):
773 w.unset_flags(gtk.CAN_FOCUS)
775 def set_controls_sensitivity(self, sensitive):
776 for button in self.forward_button, self.rewind_button, \
777 self.fforward_button, self.rrewind_button:
779 button.set_sensitive(sensitive)
781 # the play/pause button should always be available except
782 # for when the player starts without a file
783 self.play_pause_button.set_sensitive(True)
785 def on_dual_action_setting_changed( self, *args ):
786 for button in self.forward_button, self.rewind_button, \
787 self.fforward_button, self.rrewind_button, \
788 self.bookmarks_button:
790 button.set_longpress_enabled( settings.enable_dual_action_btn )
791 button.set_duration( settings.dual_action_button_delay )
793 def on_key_press(self, widget, event):
794 if platform.MAEMO:
795 if event.keyval == gtk.keysyms.Left: # seek back
796 self.do_seek( -1 * settings.seek_long )
797 elif event.keyval == gtk.keysyms.Right: # seek forward
798 self.do_seek( settings.seek_long )
799 elif event.keyval == gtk.keysyms.Return: # play/pause
800 self.on_btn_play_pause_clicked()
802 def on_player_stopped(self):
803 self.stop_progress_timer()
804 self.set_controls_sensitivity(False)
805 image(self.play_pause_button, 'media-playback-start.png')
807 def on_player_playing(self):
808 self.start_progress_timer()
809 image(self.play_pause_button, 'media-playback-pause.png')
810 self.set_controls_sensitivity(True)
811 if platform.FREMANTLE:
812 hildon.hildon_gtk_window_set_progress_indicator(\
813 self.__gui_root.main_window, False)
815 def on_player_new_track(self):
816 for widget in [self.title_label,self.artist_label,self.album_label]:
817 widget.set_markup('')
818 widget.hide()
820 self.cover_art.hide()
821 self.has_coverart = False
823 def on_player_new_metadata(self):
824 metadata = player.playlist.get_file_metadata()
825 self.set_metadata(metadata)
827 if not player.playing:
828 position = player.playlist.get_current_position()
829 estimated_length = metadata.get('length', 0)
830 self.set_progress_callback( position, estimated_length )
832 def on_player_paused( self, position, duration ):
833 self.stop_progress_timer() # This should save some power
834 self.set_progress_callback( position, duration )
835 image(self.play_pause_button, 'media-playback-start.png')
837 def on_player_end_of_playlist(self, loop):
838 pass
840 def reset_progress(self):
841 self.progress.set_fraction(0)
842 self.set_progress_callback(0,0)
844 def set_progress_callback(self, time_elapsed, total_time):
845 """ times must be in nanoseconds """
846 time_string = "%s / %s" % ( util.convert_ns(time_elapsed),
847 util.convert_ns(total_time) )
848 self.progress.set_text( time_string )
849 fraction = float(time_elapsed) / float(total_time) if total_time else 0
850 self.progress.set_fraction( fraction )
852 def on_progressbar_changed(self, widget, event):
853 if ( not settings.progress_locked and
854 event.type == gtk.gdk.BUTTON_PRESS and event.button == 1 ):
855 new_fraction = event.x/float(widget.get_allocation().width)
856 resp = player.do_seek(percent=new_fraction)
857 if resp:
858 # Preemptively update the progressbar to make seeking smoother
859 self.set_progress_callback( *resp )
861 def on_btn_play_pause_clicked(self, widget=None):
862 player.play_pause_toggle()
864 def progress_timer_callback( self ):
865 if player.playing and not player.seeking:
866 pos_int, dur_int = player.get_position_duration()
867 # This prevents bogus values from being set while seeking
868 if ( pos_int > 10**9 ) and ( dur_int > 10**9 ):
869 self.set_progress_callback( pos_int, dur_int )
870 return True
872 def start_progress_timer( self ):
873 if self.progress_timer_id is not None:
874 self.stop_progress_timer()
876 self.progress_timer_id = gobject.timeout_add(
877 1000, self.progress_timer_callback )
879 def stop_progress_timer( self ):
880 if self.progress_timer_id is not None:
881 gobject.source_remove( self.progress_timer_id )
882 self.progress_timer_id = None
884 def get_coverart_size( self ):
885 if platform.MAEMO:
886 if self.__gui_root.fullscreen:
887 size = coverart_sizes['maemo fullscreen']
888 else:
889 size = coverart_sizes['maemo']
890 else:
891 size = coverart_sizes['normal']
893 return size, size
895 def set_coverart( self, pixbuf ):
896 self.cover_art.set_from_pixbuf(pixbuf)
897 self.cover_art.show()
898 self.has_coverart = True
900 def set_metadata( self, tag_message ):
901 tags = { 'title': self.title_label, 'artist': self.artist_label,
902 'album': self.album_label }
904 # set the coverart
905 if tag_message.has_key('image') and tag_message['image'] is not None:
906 value = tag_message['image']
908 pbl = gtk.gdk.PixbufLoader()
909 try:
910 pbl.write(value)
911 pbl.close()
913 x, y = self.get_coverart_size()
914 pixbuf = pbl.get_pixbuf()
915 pixbuf = pixbuf.scale_simple( x, y, gtk.gdk.INTERP_BILINEAR )
916 self.set_coverart(pixbuf)
917 except Exception, e:
918 self.__log.exception('Error setting coverart...')
920 # set the text metadata
921 for tag,value in tag_message.iteritems():
922 if tags.has_key(tag) and value is not None and value.strip():
923 try:
924 tags[tag].set_markup('<big>'+cgi.escape(value)+'</big>')
925 except TypeError, e:
926 self.__log.exception(str(e))
927 tags[tag].set_alignment( 0.5*int(not self.has_coverart), 0.5)
928 tags[tag].show()
930 if tag == 'title':
931 # make the title bold
932 tags[tag].set_markup('<b><big>'+cgi.escape(value)+'</big></b>')
934 if not platform.MAEMO:
935 value += ' - Panucci'
937 if platform.FREMANTLE and len(value) > 25:
938 value = value[:24] + '...'
940 self.__gui_root.main_window.set_title( value )
942 def do_seek(self, seek_amount):
943 resp = player.do_seek(from_current=seek_amount*10**9)
944 if resp:
945 # Preemptively update the progressbar to make seeking smoother
946 self.set_progress_callback( *resp )
950 ##################################################
951 # PlaylistTab
952 ##################################################
953 class PlaylistTab(gtk.VBox):
954 def __init__(self, main_window):
955 gtk.VBox.__init__(self)
956 self.__log = logging.getLogger('panucci.panucci.BookmarksWindow')
957 self.main = main_window
959 self.__model = gtk.TreeStore(
960 # uid, name, position
961 gobject.TYPE_STRING, gobject.TYPE_STRING, gobject.TYPE_STRING )
963 self.set_spacing(5)
964 self.treeview = gtk.TreeView()
965 self.treeview.set_model(self.__model)
966 self.treeview.set_headers_visible(True)
967 tree_selection = self.treeview.get_selection()
968 # This breaks drag and drop, only use single selection for now
969 # tree_selection.set_mode(gtk.SELECTION_MULTIPLE)
970 tree_selection.connect('changed', self.tree_selection_changed)
972 # The tree lines look nasty on maemo
973 if platform.DESKTOP:
974 self.treeview.set_enable_tree_lines(True)
975 self.update_model()
977 ncol = gtk.TreeViewColumn(_('Name'))
978 ncell = gtk.CellRendererText()
979 ncell.set_property('ellipsize', pango.ELLIPSIZE_END)
980 ncell.set_property('editable', True)
981 ncell.connect('edited', self.label_edited)
982 ncol.set_expand(True)
983 ncol.pack_start(ncell)
984 ncol.add_attribute(ncell, 'text', 1)
986 tcol = gtk.TreeViewColumn(_('Position'))
987 tcell = gtk.CellRendererText()
988 tcol.pack_start(tcell)
989 tcol.add_attribute(tcell, 'text', 2)
991 self.treeview.append_column(ncol)
992 self.treeview.append_column(tcol)
993 self.treeview.connect('drag-data-received', self.drag_data_recieved)
994 self.treeview.connect('drag_data_get', self.drag_data_get_data)
996 treeview_targets = [
997 ( 'playlist_row_data', gtk.TARGET_SAME_WIDGET, 0 ) ]
999 self.treeview.enable_model_drag_source(
1000 gtk.gdk.BUTTON1_MASK, treeview_targets, gtk.gdk.ACTION_COPY )
1002 self.treeview.enable_model_drag_dest(
1003 treeview_targets, gtk.gdk.ACTION_COPY )
1005 sw = gtk.ScrolledWindow()
1006 sw.set_policy(gtk.POLICY_AUTOMATIC, gtk.POLICY_AUTOMATIC)
1007 sw.set_shadow_type(gtk.SHADOW_IN)
1008 sw.add(self.treeview)
1009 self.add(sw)
1011 self.hbox = gtk.HBox()
1013 self.add_button = gtk.Button(gtk.STOCK_NEW)
1014 self.add_button.set_use_stock(True)
1015 set_stock_button_text( self.add_button, _('Add File') )
1016 self.add_button.connect('clicked', self.add_file)
1017 self.hbox.pack_start(self.add_button, True, True)
1019 self.dir_button = gtk.Button(gtk.STOCK_OPEN)
1020 self.dir_button.set_use_stock(True)
1021 set_stock_button_text( self.dir_button, _('Add Directory') )
1022 self.dir_button.connect('clicked', self.add_directory)
1023 self.hbox.pack_start(self.dir_button, True, True)
1025 self.remove_button = gtk.Button(gtk.STOCK_REMOVE)
1026 self.remove_button.set_use_stock(True)
1027 self.remove_button.connect('clicked', self.remove_bookmark)
1028 self.hbox.pack_start(self.remove_button, True, True)
1030 self.jump_button = gtk.Button(gtk.STOCK_JUMP_TO)
1031 self.jump_button.set_use_stock(True)
1032 self.jump_button.connect('clicked', self.jump_bookmark)
1033 self.hbox.pack_start(self.jump_button, True, True)
1035 self.info_button = gtk.Button()
1036 self.info_button.add(
1037 gtk.image_new_from_stock(gtk.STOCK_INFO, gtk.ICON_SIZE_BUTTON))
1038 self.info_button.connect('clicked', self.show_playlist_item_details)
1039 self.hbox.pack_start(self.info_button, True, True)
1041 if platform.FREMANTLE:
1042 for child in self.hbox.get_children():
1043 if isinstance(child, gtk.Button):
1044 child.set_name('HildonButton-thumb')
1045 self.hbox.set_size_request(-1, 105)
1047 self.pack_start(self.hbox, False, True)
1049 player.playlist.register( 'file_queued',
1050 lambda x,y,z: self.update_model() )
1051 player.playlist.register( 'bookmark_added', self.on_bookmark_added )
1053 self.show_all()
1055 def tree_selection_changed(self, treeselection):
1056 count = treeselection.count_selected_rows()
1057 self.remove_button.set_sensitive(count > 0)
1058 self.jump_button.set_sensitive(count == 1)
1059 self.info_button.set_sensitive(count == 1)
1061 def drag_data_get_data(
1062 self, treeview, context, selection, target_id, timestamp):
1064 treeselection = treeview.get_selection()
1065 model, iter = treeselection.get_selected()
1066 # only allow moving around top-level parents
1067 if model.iter_parent(iter) is None:
1068 # send the path of the selected row
1069 data = model.get_string_from_iter(iter)
1070 selection.set(selection.target, 8, data)
1071 else:
1072 self.__log.debug("Can't move children...")
1074 def drag_data_recieved(
1075 self, treeview, context, x, y, selection, info, timestamp):
1077 drop_info = treeview.get_dest_row_at_pos(x, y)
1079 # TODO: If user drags the row past the last row, drop_info is None
1080 # I'm not sure if it's safe to simply assume that None is
1081 # euqivalent to the last row...
1082 if None not in [ drop_info and selection.data ]:
1083 model = treeview.get_model()
1084 path, position = drop_info
1086 from_iter = model.get_iter_from_string(selection.data)
1088 # make sure the to_iter doesn't have a parent
1089 to_iter = model.get_iter(path)
1090 if model.iter_parent(to_iter) is not None:
1091 to_iter = model.iter_parent(to_iter)
1093 from_row = model.get_path(from_iter)[0]
1094 to_row = path[0]
1096 if ( position == gtk.TREE_VIEW_DROP_BEFORE or
1097 position == gtk.TREE_VIEW_DROP_INTO_OR_BEFORE ):
1098 model.move_before( from_iter, to_iter )
1099 to_row = to_row - 1 if from_row < to_row else to_row
1100 elif ( position == gtk.TREE_VIEW_DROP_AFTER or
1101 position == gtk.TREE_VIEW_DROP_INTO_OR_AFTER ):
1102 model.move_after( from_iter, to_iter )
1103 to_row = to_row + 1 if from_row > to_row else to_row
1104 else:
1105 self.__log.debug('Drop not supported: %s', position)
1107 # don't do anything if we're not actually moving rows around
1108 if from_row != to_row:
1109 player.playlist.move_item( from_row, to_row )
1111 else:
1112 self.__log.debug('No drop_data or selection.data available')
1114 def update_model(self):
1115 plist = player.playlist
1116 path_info = self.treeview.get_path_at_pos(0,0)
1117 path = path_info[0] if path_info is not None else None
1119 self.__model.clear()
1121 # build the tree
1122 for item, data in plist.get_playlist_item_ids():
1123 parent = self.__model.append(None, (item, data.get('title'), None))
1125 for bid, bname, bpos in plist.get_bookmarks_from_item_id( item ):
1126 nice_bpos = util.convert_ns(bpos)
1127 self.__model.append( parent, (bid, bname, nice_bpos) )
1129 self.treeview.expand_all()
1131 if path is not None:
1132 self.treeview.scroll_to_cell(path)
1134 def label_edited(self, cellrenderer, path, new_text):
1135 iter = self.__model.get_iter(path)
1136 old_text = self.__model.get_value(iter, 1)
1138 if new_text.strip() and old_text != new_text:
1139 # this loop will only run once, because only one cell can be
1140 # edited at a time, we use it to get the item and bookmark ids
1141 for m, bkmk_id, biter, item_id, iiter in self.__cur_selection():
1142 self.__model.set_value(iter, 1, new_text)
1143 player.playlist.update_bookmark(
1144 item_id, bkmk_id, name=new_text )
1145 else:
1146 self.__model.set_value(iter, 1, old_text)
1148 def on_bookmark_added(self, parent_id, bookmark_name, position):
1149 self.main.notify(_('Bookmark added: %s') % bookmark_name)
1150 self.update_model()
1152 def add_file(self, widget):
1153 filename = get_file_from_filechooser(self.main.main_window)
1154 if filename is not None:
1155 player.playlist.append(filename)
1157 def add_directory(self, widget):
1158 directory = get_file_from_filechooser(
1159 self.main.main_window, folder=True )
1160 if directory is not None:
1161 player.playlist.load_directory(directory, append=True)
1163 def __cur_selection(self):
1164 selection = self.treeview.get_selection()
1165 model, bookmark_paths = selection.get_selected_rows()
1167 # Convert the paths to gtk.TreeRowReference objects, because we
1168 # might modify the model while this generator is running
1169 bookmark_refs = [gtk.TreeRowReference(model, p) for p in bookmark_paths]
1171 for reference in bookmark_refs:
1172 bookmark_iter = model.get_iter(reference.get_path())
1173 item_iter = model.iter_parent(bookmark_iter)
1175 # bookmark_iter is actually an item_iter
1176 if item_iter is None:
1177 item_iter = bookmark_iter
1178 item_id = model.get_value(item_iter, 0)
1179 bookmark_id, bookmark_iter = None, None
1180 else:
1181 bookmark_id = model.get_value(bookmark_iter, 0)
1182 item_id = model.get_value(item_iter, 0)
1184 yield model, bookmark_id, bookmark_iter, item_id, item_iter
1186 def remove_bookmark(self, w=None):
1187 for model, bkmk_id, bkmk_iter, item_id, item_iter in self.__cur_selection():
1188 player.playlist.remove_bookmark( item_id, bkmk_id )
1189 if bkmk_iter is not None:
1190 model.remove(bkmk_iter)
1191 elif item_iter is not None:
1192 model.remove(item_iter)
1194 def select_current_item(self):
1195 model = self.treeview.get_model()
1196 selection = self.treeview.get_selection()
1197 current_item_id = str(player.playlist.get_current_item())
1198 for row in iter(model):
1199 if model.get_value(row.iter, 0) == current_item_id:
1200 selection.unselect_all()
1201 self.treeview.set_cursor(row.path)
1202 self.treeview.scroll_to_cell(row.path, use_align=True)
1203 break
1205 def show_playlist_item_details(self, w):
1206 selection = self.treeview.get_selection()
1207 if selection.count_selected_rows() == 1:
1208 selected = self.__cur_selection().next()
1209 model, bkmk_id, bkmk_iter, item_id, item_iter = selected
1210 playlist_item = player.playlist.get_item_by_id(item_id)
1211 PlaylistItemDetails(self.main, playlist_item)
1213 def jump_bookmark(self, w):
1214 selected = list(self.__cur_selection())
1215 if len(selected) == 1:
1216 # It should be guranteed by the fact that we only enable the
1217 # "Jump to" button when the selection count equals 1.
1218 model, bkmk_id, bkmk_iter, item_id, item_iter = selected.pop(0)
1219 player.playlist.load_from_bookmark_id(item_id, bkmk_id)
1221 # FIXME: The player/playlist should be able to take care of this
1222 if not player.playing:
1223 player.play()
1226 ##################################################
1227 # PlaylistItemDetails
1228 ##################################################
1229 class PlaylistItemDetails(gtk.Dialog):
1230 def __init__(self, main, playlist_item):
1231 gtk.Dialog.__init__(self, _('Playlist item details'),
1232 main.main_window, gtk.DIALOG_MODAL)
1234 if not platform.FREMANTLE:
1235 self.add_button(gtk.STOCK_CLOSE, gtk.RESPONSE_OK)
1237 self.main = main
1238 self.fill(playlist_item)
1239 self.set_has_separator(False)
1240 self.set_resizable(False)
1241 self.show_all()
1242 self.run()
1243 self.destroy()
1245 def fill(self, playlist_item):
1246 t = gtk.Table(10, 2)
1247 self.vbox.pack_start(t, expand=False)
1249 metadata = playlist_item.metadata
1251 t.attach(gtk.Label(_('Custom title:')), 0, 1, 0, 1)
1252 t.attach(gtk.Label(_('ID:')), 0, 1, 1, 2)
1253 t.attach(gtk.Label(_('Playlist ID:')), 0, 1, 2, 3)
1254 t.attach(gtk.Label(_('Filepath:')), 0, 1, 3, 4)
1256 row_num = 4
1257 for key in metadata:
1258 if metadata[key] is not None:
1259 t.attach( gtk.Label(key.capitalize()+':'),
1260 0, 1, row_num, row_num+1 )
1261 row_num += 1
1263 t.foreach(lambda x, y: x.set_alignment(1, 0.5), None)
1264 t.foreach(lambda x, y: x.set_markup('<b>%s</b>' % x.get_label()), None)
1266 t.attach(gtk.Label(playlist_item.title or _('<not modified>')),1,2,0,1)
1267 t.attach(gtk.Label(str(playlist_item)), 1, 2, 1, 2)
1268 t.attach(gtk.Label(playlist_item.playlist_id), 1, 2, 2, 3)
1269 t.attach(gtk.Label(playlist_item.filepath), 1, 2, 3, 4)
1271 row_num = 4
1272 for key in metadata:
1273 value = metadata[key]
1274 if key == 'length':
1275 value = util.convert_ns(value)
1276 if metadata[key] is not None:
1277 t.attach( gtk.Label( str(value) or _('<not set>')),
1278 1, 2, row_num, row_num+1)
1279 row_num += 1
1281 t.foreach(lambda x, y: x.get_alignment() == (0.5, 0.5) and \
1282 x.set_alignment(0, 0.5), None)
1284 t.set_border_width(8)
1285 t.set_row_spacings(4)
1286 t.set_col_spacings(8)
1288 l = gtk.ListStore(str, str)
1289 t = gtk.TreeView(l)
1290 cr = gtk.CellRendererText()
1291 cr.set_property('ellipsize', pango.ELLIPSIZE_END)
1292 c = gtk.TreeViewColumn(_('Title'), cr, text=0)
1293 c.set_expand(True)
1294 t.append_column(c)
1295 c = gtk.TreeViewColumn(_('Time'), gtk.CellRendererText(), text=1)
1296 t.append_column(c)
1297 playlist_item.load_bookmarks()
1298 for bookmark in playlist_item.bookmarks:
1299 l.append([bookmark.bookmark_name, \
1300 util.convert_ns(bookmark.seek_position)])
1302 sw = gtk.ScrolledWindow()
1303 sw.set_shadow_type(gtk.SHADOW_IN)
1304 sw.add(t)
1305 sw.set_policy(gtk.POLICY_AUTOMATIC, gtk.POLICY_AUTOMATIC)
1306 e = gtk.Expander(_('Bookmarks'))
1307 e.add(sw)
1308 if not platform.MAEMO:
1309 self.vbox.pack_start(e)
1312 def run(filename=None):
1313 PanucciGUI(filename)
1314 gtk.main()
1316 if __name__ == '__main__':
1317 log.error( 'Use the "panucci" executable to run this program.' )
1318 log.error( 'Exiting...' )
1319 sys.exit(1)