moved write_config to util
[panucci.git] / src / panucci / gtkui / gtkmain.py
blobc4eb962325688885496d04c3a5b5455a47ae7a35
1 # -*- coding: utf-8 -*-
3 # This file is part of Panucci.
4 # Copyright (c) 2008-2011 The Panucci 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 from __future__ import absolute_import
21 import logging
22 import os.path
23 import cgi
24 import gtk
25 import gobject
26 import pango
28 import panucci
29 from panucci import util
30 from panucci import platform
31 from panucci import playlist
32 from panucci.dbusinterface import interface
33 from panucci.services import ObservableService
34 from panucci.gtkui import gtkwidgets as widgets
35 from panucci.gtkui import gtkplaylist
36 from panucci.gtkui import gtkutil
38 try:
39 import pynotify
40 pynotify.init('Panucci')
41 have_pynotify = True
42 except:
43 have_pynotify = False
45 try:
46 import hildon
47 except:
48 if platform.MAEMO:
49 log = logging.getLogger('panucci.panucci')
50 log.critical( 'Using GTK widgets, install "python2.5-hildon" '
51 'for this to work properly.' )
53 if platform.FREMANTLE:
54 # Workaround Maemo bug 6694 (Playback in Silent mode)
55 gobject.set_application_name('FMRadio')
57 gtk.icon_size_register('panucci-button', 32, 32)
59 ##################################################
60 # PanucciGUI
61 ##################################################
62 class PanucciGUI(object):
63 """ The object that holds the entire panucci gui """
65 def __init__(self, settings, filename=None):
66 self.__log = logging.getLogger('panucci.panucci.PanucciGUI')
67 interface.register_gui(self)
68 self.config = settings.config
69 self.playlist = playlist.Playlist(self.config)
71 # Build the base ui (window and menubar)
72 if platform.MAEMO:
73 self.app = hildon.Program()
74 if platform.FREMANTLE:
75 window = hildon.StackableWindow()
76 else:
77 window = hildon.Window()
78 self.app.add_window(window)
79 else:
80 window = gtk.Window(gtk.WINDOW_TOPLEVEL)
82 self.main_window = window
83 window.set_title('Panucci')
84 self.window_icon = util.find_data_file('panucci.png')
85 if self.window_icon is not None:
86 window.set_icon_from_file( self.window_icon )
87 window.set_default_size(400, -1)
88 window.set_border_width(0)
89 window.connect("destroy", self.destroy)
91 # Add the tabs (they are private to prevent us from trying to do
92 # something like gui_root.player_tab.some_function() from inside
93 # playlist_tab or vice-versa)
94 self.__player_tab = PlayerTab(self)
95 self.__playlist_tab = gtkplaylist.PlaylistTab(self, self.playlist)
97 self.create_actions()
99 if platform.FREMANTLE:
100 self.playlist_window = hildon.StackableWindow()
101 self.playlist_window.set_app_menu(self.create_playlist_app_menu())
102 else:
103 self.playlist_window = gtk.Window(gtk.WINDOW_TOPLEVEL)
104 self.playlist_window.connect('delete-event', gtk.Widget.hide_on_delete)
105 self.playlist_window.set_title(_('Playlist'))
106 self.playlist_window.set_transient_for(self.main_window)
107 self.playlist_window.add(self.__playlist_tab)
109 if platform.MAEMO:
110 if platform.FREMANTLE:
111 window.set_app_menu(self.create_app_menu())
112 else:
113 window.set_menu(self.create_menu())
114 window.add(self.__player_tab)
115 else:
116 menu_vbox = gtk.VBox()
117 menu_vbox.set_spacing(0)
118 window.add(menu_vbox)
119 menu_bar = gtk.MenuBar()
120 self.create_desktop_menu(menu_bar)
121 menu_vbox.pack_start(menu_bar, False, False, 0)
122 menu_bar.show()
123 menu_vbox.pack_end(self.__player_tab, True, True, 6)
125 # Tie it all together!
126 self.__ignore_queue_check = False
127 self.__window_fullscreen = False
129 if platform.MAEMO and interface.headset_device:
130 # Enable play/pause with headset button
131 import dbus
132 interface.headset_device.connect_to_signal('Condition', \
133 self.handle_headset_button)
134 system_bus = dbus.SystemBus()
136 # Monitor connection state of BT headset
137 # I haven't seen this option before "settings.play_on_headset"
138 PATH = '/org/freedesktop/Hal/devices/computer_logicaldev_input_1'
139 def handler_func(device_path):
140 if device_path == PATH and settings.play_on_headset and not self.playlist.player.playing:
141 self.playlist.player.play()
142 system_bus.add_signal_receiver(handler_func, 'DeviceAdded', \
143 'org.freedesktop.Hal.Manager', None, \
144 '/org/freedesktop/Hal/Manager')
145 # End Monitor connection state of BT headset
147 # Monitor BT headset buttons
148 def handle_bt_button(signal, button):
149 # See http://bugs.maemo.org/8283 for details
150 if signal == 'ButtonPressed':
151 if button == 'play-cd':
152 self.playlist.player.play_pause_toggle()
153 elif button == 'pause-cd':
154 self.playlist.player.pause()
155 elif button == 'next-song':
156 self.__player_tab.do_seek(self.config.getint("options", "seek_short"))
157 elif button == 'previous-song':
158 self.__player_tab.do_seek(-1*self.config.getint("options", "seek_short"))
160 system_bus.add_signal_receiver(handle_bt_button, 'Condition', \
161 'org.freedesktop.Hal.Device', None, PATH)
162 # End Monitor BT headset buttons
164 self.main_window.connect('key-press-event', self.on_key_press)
165 self.playlist.register( 'file_queued', self.on_file_queued )
167 self.playlist.register( 'playlist-to-be-overwritten',
168 self.check_queue )
169 self.__player_tab.register( 'select-current-item-request',
170 self.__select_current_item )
172 self.main_window.show_all()
174 # this should be done when the gui is ready
175 self.playlist.player.init(filepath=filename)
177 pos_int, dur_int = self.playlist.player.get_position_duration()
178 # This prevents bogus values from being set while seeking
179 if (pos_int > 10**9) and (dur_int > 10**9):
180 self.set_progress_callback(pos_int, dur_int)
182 gtk.main()
184 def create_actions(self):
185 # File menu
186 self.action_open = gtk.Action('open_file', _('Add File'), _('Open a file or playlist'), gtk.STOCK_NEW)
187 self.action_open.connect('activate', self.open_file_callback)
188 self.action_open_dir = gtk.Action('open_dir', _('Add Folder'), _('Open a directory'), gtk.STOCK_OPEN)
189 self.action_open_dir.connect('activate', self.open_dir_callback)
190 self.action_save = gtk.Action('save', _('Save Playlist'), _('Save current playlist to file'), gtk.STOCK_SAVE_AS)
191 self.action_save.connect('activate', self.save_to_playlist_callback)
192 self.action_empty_playlist = gtk.Action('empty_playlist', _('Clear Playlist'), _('Clear current playlist'), gtk.STOCK_DELETE)
193 self.action_empty_playlist.connect('activate', self.empty_playlist_callback)
194 self.action_delete_bookmarks = gtk.Action('delete_bookmarks', _('Delete All Bookmarks'), _('Deleting all bookmarks'), gtk.STOCK_DELETE)
195 self.action_delete_bookmarks.connect('activate', self.delete_all_bookmarks_callback)
196 self.action_quit = gtk.Action('quit', _('Quit'), _('Close Panucci'), gtk.STOCK_QUIT)
197 self.action_quit.connect('activate', self.destroy)
198 # Tools menu
199 self.action_playlist = gtk.Action('playlist', _('Playlist'), _('Open the current playlist'), None)
200 self.action_playlist.connect('activate', lambda a: self.playlist_window.show())
201 self.action_settings = gtk.Action('settings', _('Settings'), _('Open the settings dialog'), None)
202 self.action_settings.connect('activate', self.settings_callback)
203 # Settings menu
204 self.action_lock_progress = gtk.ToggleAction('lock_progress', 'Lock Progress Bar', None, None)
205 self.action_lock_progress.connect("activate", self.set_boolean_config_callback)
206 self.action_lock_progress.set_active(self.config.getboolean("options", "lock_progress"))
207 self.action_dual_action_button = gtk.ToggleAction('dual_action_button', 'Dual Action Button', None, None)
208 self.action_dual_action_button.connect("activate", self.set_boolean_config_callback)
209 self.action_dual_action_button.set_active(self.config.getboolean("options", "dual_action_button"))
210 self.action_stay_at_end = gtk.ToggleAction('stay_at_end', 'Stay at End', None, None)
211 self.action_stay_at_end.connect("activate", self.set_boolean_config_callback)
212 self.action_stay_at_end.set_active(self.config.getboolean("options", "stay_at_end"))
213 self.action_seek_back = gtk.ToggleAction('seek_back', 'Seek Back', None, None)
214 self.action_seek_back.connect("activate", self.set_boolean_config_callback)
215 self.action_seek_back.set_active(self.config.getboolean("options", "seek_back"))
216 self.action_scrolling_labels = gtk.ToggleAction('scrolling_labels', 'Scrolling Labels', None, None)
217 self.action_scrolling_labels.connect("activate", self.scrolling_labels_callback)
218 self.action_scrolling_labels.set_active(self.config.getboolean("options", "scrolling_labels"))
219 self.action_play_mode = gtk.Action('play_mode', 'Play Mode', None, None)
220 self.action_play_mode_all = gtk.RadioAction('all', 'All', None, None, 0)
221 self.action_play_mode_all.connect("activate", self.set_play_mode_callback)
222 self.action_play_mode_single = gtk.RadioAction('single', 'Single', None, None, 1)
223 self.action_play_mode_single.connect("activate", self.set_play_mode_callback)
224 self.action_play_mode_single.set_group(self.action_play_mode_all)
225 self.action_play_mode_random = gtk.RadioAction('random', 'Random', None, None, 1)
226 self.action_play_mode_random.connect("activate", self.set_play_mode_callback)
227 self.action_play_mode_random.set_group(self.action_play_mode_all)
228 self.action_play_mode_repeat = gtk.RadioAction('repeat', 'Repeat', None, None, 1)
229 self.action_play_mode_repeat.connect("activate", self.set_play_mode_callback)
230 self.action_play_mode_repeat.set_group(self.action_play_mode_all)
231 if self.config.get("options", "play_mode") == "single":
232 self.action_play_mode_single.set_active(True)
233 elif self.config.get("options", "play_mode") == "random":
234 self.action_play_mode_random.set_active(True)
235 elif self.config.get("options", "play_mode") == "repeat":
236 self.action_play_mode_repeat.set_active(True)
237 else:
238 self.action_play_mode_all.set_active(True)
239 # Help menu
240 self.action_about = gtk.Action('about', _('About'), _('Show application version'), gtk.STOCK_ABOUT)
241 self.action_about.connect('activate', self.about_callback)
243 def create_desktop_menu(self, menu_bar):
244 file_menu_item = gtk.MenuItem(_('File'))
245 file_menu = gtk.Menu()
246 file_menu.append(self.action_open.create_menu_item())
247 file_menu.append(self.action_open_dir.create_menu_item())
248 file_menu.append(self.action_save.create_menu_item())
249 file_menu.append(self.action_empty_playlist.create_menu_item())
250 file_menu.append(self.action_delete_bookmarks.create_menu_item())
251 file_menu.append(gtk.SeparatorMenuItem())
252 file_menu.append(self.action_quit.create_menu_item())
253 file_menu_item.set_submenu(file_menu)
254 menu_bar.append(file_menu_item)
256 tools_menu_item = gtk.MenuItem(_('Tools'))
257 tools_menu = gtk.Menu()
258 tools_menu.append(self.action_playlist.create_menu_item())
259 #tools_menu.append(self.action_settings.create_menu_item())
260 tools_menu_item.set_submenu(tools_menu)
261 menu_bar.append(tools_menu_item)
263 settings_menu_item = gtk.MenuItem(_('Settings'))
264 settings_menu = gtk.Menu()
265 settings_menu.append(self.action_lock_progress.create_menu_item())
266 settings_menu.append(self.action_dual_action_button.create_menu_item())
267 settings_menu.append(self.action_stay_at_end.create_menu_item())
268 settings_menu.append(self.action_seek_back.create_menu_item())
269 settings_menu.append(self.action_scrolling_labels.create_menu_item())
270 play_mode_menu_item = self.action_play_mode.create_menu_item()
271 settings_menu.append(play_mode_menu_item)
272 play_mode_menu = gtk.Menu()
273 play_mode_menu_item.set_submenu(play_mode_menu)
274 play_mode_menu.append(self.action_play_mode_all.create_menu_item())
275 play_mode_menu.append(self.action_play_mode_single.create_menu_item())
276 play_mode_menu.append(self.action_play_mode_random.create_menu_item())
277 play_mode_menu.append(self.action_play_mode_repeat.create_menu_item())
278 settings_menu_item.set_submenu(settings_menu)
279 menu_bar.append(settings_menu_item)
281 help_menu_item = gtk.MenuItem(_('Help'))
282 help_menu = gtk.Menu()
283 help_menu.append(self.action_about.create_menu_item())
284 help_menu_item.set_submenu(help_menu)
285 menu_bar.append(help_menu_item)
287 def create_playlist_app_menu(self):
288 menu = hildon.AppMenu()
290 for action in (self.action_save,
291 self.action_delete_bookmarks):
292 b = gtk.Button()
293 action.connect_proxy(b)
294 menu.append(b)
296 menu.show_all()
297 return menu
299 def create_app_menu(self):
300 menu = hildon.AppMenu()
302 for action in (self.action_settings,
303 self.action_playlist,
304 self.action_open,
305 self.action_open_dir,
306 self.action_empty_playlist,
307 self.action_about):
308 b = gtk.Button()
309 action.connect_proxy(b)
310 menu.append(b)
312 menu.show_all()
313 return menu
315 def create_menu(self):
316 # the main menu
317 menu = gtk.Menu()
319 menu_open = gtk.ImageMenuItem(_('Add File'))
320 menu_open.set_image(
321 gtk.image_new_from_stock(gtk.STOCK_NEW, gtk.ICON_SIZE_MENU))
322 menu_open.connect("activate", self.open_file_callback)
323 menu.append(menu_open)
325 menu_open = gtk.ImageMenuItem(_('Add Folder'))
326 menu_open.set_image(
327 gtk.image_new_from_stock(gtk.STOCK_OPEN, gtk.ICON_SIZE_MENU))
328 menu_open.connect("activate", self.open_dir_callback)
329 menu.append(menu_open)
331 # the recent files menu
332 self.menu_recent = gtk.MenuItem(_('Open recent playlist'))
333 menu.append(self.menu_recent)
334 self.create_recent_files_menu()
336 menu.append(gtk.SeparatorMenuItem())
338 menu_save = gtk.ImageMenuItem(_('Save current playlist'))
339 menu_save.set_image(
340 gtk.image_new_from_stock(gtk.STOCK_SAVE_AS, gtk.ICON_SIZE_MENU))
341 menu_save.connect("activate", self.save_to_playlist_callback)
342 menu.append(menu_save)
344 menu_save = gtk.ImageMenuItem(_('Delete Playlist'))
345 menu_save.set_image(
346 gtk.image_new_from_stock(gtk.STOCK_DELETE, gtk.ICON_SIZE_MENU))
347 menu_save.connect("activate", self.empty_playlist_callback)
348 menu.append(menu_save)
350 menu_save = gtk.ImageMenuItem(_('Delete All Bookmarks'))
351 menu_save.set_image(
352 gtk.image_new_from_stock(gtk.STOCK_DELETE, gtk.ICON_SIZE_MENU))
353 menu_save.connect("activate", self.delete_all_bookmarks_callback)
354 menu.append(menu_save)
356 menu.append(gtk.SeparatorMenuItem())
358 # the settings sub-menu
359 menu_settings = gtk.MenuItem(_('Settings'))
360 menu.append(menu_settings)
362 menu_settings_sub = gtk.Menu()
363 menu_settings.set_submenu(menu_settings_sub)
365 menu_settings_enable_dual_action = gtk.CheckMenuItem(_('Enable dual-action buttons') )
366 menu_settings_enable_dual_action.connect('toggled', self.set_dual_action_button_callback)
367 menu_settings_enable_dual_action.set_active(self.config.getboolean("options", "dual_action_button"))
368 menu_settings_sub.append(menu_settings_enable_dual_action)
370 menu_settings_lock_progress = gtk.CheckMenuItem(_('Lock Progress Bar'))
371 menu_settings_lock_progress.connect('toggled', self.lock_progress_callback)
372 menu_settings_lock_progress.set_active(self.config.getboolean("options", "lock_progress"))
373 menu_settings_sub.append(menu_settings_lock_progress)
375 menu_about = gtk.ImageMenuItem(gtk.STOCK_ABOUT)
376 menu_about.connect("activate", self.about_callback)
377 menu.append(menu_about)
379 menu.append(gtk.SeparatorMenuItem())
381 menu_quit = gtk.ImageMenuItem(gtk.STOCK_QUIT)
382 menu_quit.connect("activate", self.destroy)
383 menu.append(menu_quit)
385 return menu
387 def create_recent_files_menu( self ):
388 max_files = self.config.getint("options", "max_recent_files")
389 self.recent_files = player.playlist.get_recent_files(max_files)
390 menu_recent_sub = gtk.Menu()
392 if len(self.recent_files) > 0:
393 for f in self.recent_files:
394 # don't include the temporary playlist in the file list
395 if f == panucci.PLAYLIST_FILE: continue
396 # don't include non-existant files
397 if not os.path.exists( f ): continue
398 filename, extension = os.path.splitext(os.path.basename(f))
399 menu_item = gtk.MenuItem( filename.replace('_', ' '))
400 menu_item.connect('activate', self.on_recent_file_activate, f)
401 menu_recent_sub.append(menu_item)
402 else:
403 menu_item = gtk.MenuItem(_('No recent files available.'))
404 menu_item.set_sensitive(False)
405 menu_recent_sub.append(menu_item)
407 self.menu_recent.set_submenu(menu_recent_sub)
409 def notify(self, message):
410 """ Sends a notification using pynotify, returns message """
411 if platform.DESKTOP and have_pynotify:
412 icon = util.find_data_file('panucci_64x64.png')
413 notification = pynotify.Notification(self.main_window.get_title(), message, icon)
414 notification.show()
415 elif platform.FREMANTLE:
416 hildon.hildon_banner_show_information(self.main_window, \
417 '', message)
418 elif platform.MAEMO:
419 # Note: This won't work if we're not in the gtk main loop
420 markup = '<b>%s</b>\n<small>%s</small>' % (self.main_window.get_title(), message)
421 hildon.hildon_banner_show_information_with_markup(self.main_window, None, markup)
423 def destroy(self, widget):
424 self.main_window.hide()
425 self.playlist.player.quit()
426 util.write_config(self.config)
427 gtk.main_quit()
429 def set_progress_indicator(self, loading_title=False):
430 if platform.FREMANTLE:
431 if loading_title:
432 self.main_window.set_title(_('Loading...'))
433 hildon.hildon_gtk_window_set_progress_indicator(self.main_window, \
434 True)
435 while gtk.events_pending():
436 gtk.main_iteration(False)
438 def show_main_window(self):
439 self.main_window.present()
441 def check_queue(self):
442 """ Makes sure the queue is saved if it has been modified
443 True means a new file can be opened
444 False means the user does not want to continue """
446 if not self.__ignore_queue_check and self.playlist.queue_modified:
447 response = gtkutil.dialog(
448 self.main_window, _('Save current playlist'),
449 _('Current playlist has been modified'),
450 _('Opening a new file will replace the current playlist. ') +
451 _('Do you want to save it before creating a new one?'),
452 affirmative_button=gtk.STOCK_SAVE,
453 negative_button=_('Discard changes'))
455 self.__log.debug('Response to "Save Queue?": %s', response)
457 if response is None:
458 return False
459 elif response:
460 return self.save_to_playlist_callback()
461 elif not response:
462 return True
463 else:
464 return False
465 else:
466 return True
468 def open_file_callback(self, widget=None):
469 # set __ingnore__queue_check because we already did the check
470 self.__ignore_queue_check = True
471 filename = gtkutil.get_file_from_filechooser(self)
472 if filename is not None:
473 self._play_file(filename)
475 self.__ignore_queue_check = False
477 def open_dir_callback(self, widget=None):
478 filename = gtkutil.get_file_from_filechooser(self, folder=True)
479 if filename is not None:
480 self._play_file(filename)
482 def save_to_playlist_callback(self, widget=None):
483 filename = gtkutil.get_file_from_filechooser(
484 self, save_file=True, save_to='playlist.m3u' )
486 if filename is None:
487 return False
489 if os.path.isfile(filename):
490 response = gtkutil.dialog( self.main_window, _('File already exists'),
491 _('File already exists'),
492 _('The file %s already exists. You can choose another name or '
493 'overwrite the existing file.') % os.path.basename(filename),
494 affirmative_button=gtk.STOCK_SAVE,
495 negative_button=_('Rename file'))
497 if response is None:
498 return None
500 elif response:
501 pass
502 elif not response:
503 return self.save_to_playlist_callback()
505 ext = util.detect_filetype(filename)
506 if not self.playlist.save_to_new_playlist(filename, ext):
507 self.notify(_('Error saving playlist...'))
508 return False
510 return True
512 def empty_playlist_callback(self, w):
513 self.playlist.reset_playlist()
514 self.__playlist_tab.treeview.get_model().clear()
516 def delete_all_bookmarks_callback(self, widget=None):
517 response = gtkutil.dialog(
518 self.main_window, _('Delete All Bookmarks'),
519 _('Would you like to delete all bookmarks?'),
520 _('By accepting all bookmarks in the database will be deleted.'),
521 negative_button=None)
523 self.__log.debug('Response to "Delete all bookmarks?": %s', response)
525 if response:
526 self.playlist.delete_all_bookmarks()
527 model = self.__playlist_tab.treeview.get_model()
529 for row in iter(model):
530 while model.iter_has_child(row.iter):
531 bkmk_iter = model.iter_children(row.iter)
532 model.remove(bkmk_iter)
534 def set_boolean_config_callback(self, w):
535 if w.get_active():
536 self.config.set("options", w.get_name(), "true")
537 else:
538 self.config.set("options", w.get_name(), "false")
540 def scrolling_labels_callback(self, w):
541 self.set_boolean_config_callback(w)
542 self.__player_tab.title_label.scrolling = w.get_active()
544 def set_play_mode_callback(self, w):
545 self.config.set("options", "play_mode", w.get_name())
547 def __get_fullscreen(self):
548 return self.__window_fullscreen
550 def __set_fullscreen(self, value):
551 if value != self.__window_fullscreen:
552 if value:
553 self.main_window.fullscreen()
554 else:
555 self.main_window.unfullscreen()
557 self.__window_fullscreen = value
558 self.playlist.send_metadata()
560 fullscreen = property( __get_fullscreen, __set_fullscreen )
562 def on_key_press(self, widget, event):
563 if platform.MAEMO:
564 if event.keyval == gtk.keysyms.F6:
565 self.fullscreen = not self.fullscreen
567 def on_recent_file_activate(self, widget, filepath):
568 self._play_file(filepath)
570 def on_file_queued(self, filepath, success, notify):
571 if notify:
572 filename = os.path.basename(filepath)
573 if success:
574 self.__log.info(
575 self.notify( '%s added successfully.' % filename ))
576 else:
577 self.__log.error(
578 self.notify( 'Error adding %s to the queue.' % filename))
580 def settings_callback(self, widget):
581 from panucci.gtkui.gtksettingsdialog import SettingsDialog
582 SettingsDialog(self)
584 def about_callback(self, widget):
585 if platform.FREMANTLE:
586 from panucci.gtkui.gtkaboutdialog import HeAboutDialog
587 HeAboutDialog.present(self.main_window, panucci.__version__)
588 else:
589 from panucci.gtkui.gtkaboutdialog import AboutDialog
590 AboutDialog(self.main_window, panucci.__version__)
592 def _play_file(self, filename, pause_on_load=False):
593 self.playlist.load( os.path.abspath(filename) )
595 if self.playlist.is_empty:
596 return False
598 def handle_headset_button(self, event, button):
599 if event == 'ButtonPressed' and button == 'phone':
600 self.playlist.player.play_pause_toggle()
602 def __select_current_item( self ):
603 # Select the currently playing track in the playlist tab
604 # and switch to it (so we can edit bookmarks, etc.. there)
605 self.__playlist_tab.select_current_item()
606 self.playlist_window.show()
608 ##################################################
609 # PlayerTab
610 ##################################################
611 class PlayerTab(ObservableService, gtk.HBox):
612 """ The tab that holds the player elements """
614 signals = [ 'select-current-item-request', ]
616 def __init__(self, gui_root):
617 self.__log = logging.getLogger('panucci.panucci.PlayerTab')
618 self.__gui_root = gui_root
619 self.config = gui_root.config
620 self.playlist = gui_root.playlist
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 self.playlist.player.register( 'stopped', self.on_player_stopped )
640 self.playlist.player.register( 'playing', self.on_player_playing )
641 self.playlist.player.register( 'paused', self.on_player_paused )
642 self.playlist.player.register( 'eof', self.on_player_eof )
643 self.playlist.register( 'end-of-playlist',
644 self.on_player_end_of_playlist )
645 self.playlist.register( 'new-track-loaded',
646 self.on_player_new_track )
647 self.playlist.register( 'new-metadata-available',
648 self.on_player_new_metadata )
649 self.playlist.register( 'reset-playlist',
650 self.on_player_reset_playlist )
652 def make_player_tab(self):
653 main_vbox = gtk.VBox()
654 main_vbox.set_spacing(6)
655 # add a vbox to self
656 self.pack_start(main_vbox, True, True)
658 # a hbox to hold the cover art and metadata vbox
659 metadata_hbox = gtk.HBox()
660 metadata_hbox.set_spacing(6)
661 main_vbox.pack_start(metadata_hbox, True, False)
663 self.cover_art = gtk.Image()
664 metadata_hbox.pack_start( self.cover_art, False, False )
666 # vbox to hold metadata
667 metadata_vbox = gtk.VBox()
668 metadata_vbox.pack_start(gtk.Image(), True, True)
669 self.artist_label = gtk.Label('')
670 self.artist_label.set_ellipsize(pango.ELLIPSIZE_END)
671 metadata_vbox.pack_start(self.artist_label, False, False)
672 separator = gtk.Label("")
673 separator.set_size_request(-1, 10)
674 metadata_vbox.pack_start(separator, False, False)
675 self.album_label = gtk.Label('')
676 if platform.FREMANTLE:
677 hildon.hildon_helper_set_logical_font(self.album_label, 'SmallSystemFont')
678 hildon.hildon_helper_set_logical_color(self.album_label, gtk.RC_FG, gtk.STATE_NORMAL, 'SecondaryTextColor')
679 else:
680 self.album_label.modify_font(pango.FontDescription('normal 8'))
681 self.album_label.set_ellipsize(pango.ELLIPSIZE_END)
682 metadata_vbox.pack_start(self.album_label, False, False)
683 separator = gtk.Label("")
684 separator.set_size_request(-1, 10)
685 metadata_vbox.pack_start(separator, False, False)
686 self.title_label = widgets.ScrollingLabel('',
687 self.config.get("options", "scrolling_color"),
688 update_interval=100,
689 pixel_jump=1,
690 delay_btwn_scrolls=5000,
691 delay_halfway=3000)
692 self.title_label.scrolling = self.config.getboolean("options", "scrolling_labels")
693 metadata_vbox.pack_start(self.title_label, False, False)
694 metadata_vbox.pack_start(gtk.Image(), True, True)
695 metadata_hbox.pack_start( metadata_vbox, True, True )
697 progress_eventbox = gtk.EventBox()
698 progress_eventbox.set_events(gtk.gdk.BUTTON_PRESS_MASK)
699 progress_eventbox.connect(
700 'button-press-event', self.on_progressbar_changed )
701 self.progress = gtk.ProgressBar()
702 self.progress.set_size_request(-1, self.config.getint("options", "progress_height"))
703 progress_eventbox.add(self.progress)
704 main_vbox.pack_start( progress_eventbox, False, False )
706 # make the button box
707 buttonbox = gtk.HBox()
709 # A wrapper to help create DualActionButtons with the right settings
710 def create_da(widget, action, widget2=None, action2=None):
711 if platform.FREMANTLE:
712 widget2 = None
713 action2 = None
715 return widgets.DualActionButton(widget, action, self.config, widget2, action2)
717 self.rrewind_button = create_da(
718 gtkutil.generate_image('media-skip-backward.png'),
719 lambda: self.do_seek(-1*self.config.getint('options', 'seek_long')),
720 gtkutil.generate_image(gtk.STOCK_GOTO_FIRST, True),
721 self.playlist.prev)
722 buttonbox.add(self.rrewind_button)
724 self.rewind_button = create_da(
725 gtkutil.generate_image('media-seek-backward.png'),
726 lambda: self.do_seek(-1*self.config.getint('options', 'seek_short')))
727 buttonbox.add(self.rewind_button)
729 self.play_pause_button = gtk.Button('')
730 gtkutil.image(self.play_pause_button, 'media-playback-start.png')
731 self.play_pause_button.connect( 'clicked',
732 self.on_btn_play_pause_clicked )
733 self.play_pause_button.set_sensitive(False)
734 buttonbox.add(self.play_pause_button)
736 self.forward_button = create_da(
737 gtkutil.generate_image('media-seek-forward.png'),
738 lambda: self.do_seek(self.config.getint('options', 'seek_short')))
739 buttonbox.add(self.forward_button)
741 self.fforward_button = create_da(
742 gtkutil.generate_image('media-skip-forward.png'),
743 lambda: self.do_seek(self.config.getint('options', 'seek_long')),
744 gtkutil.generate_image(gtk.STOCK_GOTO_LAST, True),
745 self.playlist.next)
746 buttonbox.add(self.fforward_button)
748 self.bookmarks_button = create_da(
749 gtkutil.generate_image('bookmark-new.png'),
750 self.playlist.player.add_bookmark_at_current_position,
751 gtkutil.generate_image(gtk.STOCK_JUMP_TO, True),
752 lambda *args: self.notify('select-current-item-request'))
753 buttonbox.add(self.bookmarks_button)
754 self.set_controls_sensitivity(False)
756 if platform.FREMANTLE:
757 for child in buttonbox.get_children():
758 if isinstance(child, gtk.Button):
759 child.set_name('HildonButton-thumb')
761 buttonbox.set_size_request(-1, self.config.getint("options", "button_height"))
762 main_vbox.pack_start(buttonbox, False, False)
764 if platform.MAEMO:
765 self.__gui_root.main_window.connect( 'key-press-event',
766 self.on_key_press )
768 # Disable focus for all widgets, so we can use the cursor
769 # keys + enter to directly control our media player, which
770 # is handled by "key-press-event"
771 for w in (
772 self.rrewind_button, self.rewind_button,
773 self.play_pause_button, self.forward_button,
774 self.fforward_button, self.progress,
775 self.bookmarks_button, ):
776 w.unset_flags(gtk.CAN_FOCUS)
778 def set_controls_sensitivity(self, sensitive):
779 for button in self.forward_button, self.rewind_button, \
780 self.fforward_button, self.rrewind_button:
782 button.set_sensitive(sensitive)
784 # the play/pause button should always be available except
785 # for when the player starts without a file
786 self.play_pause_button.set_sensitive(True)
788 def on_dual_action_setting_changed( self, *args ):
789 for button in self.forward_button, self.rewind_button, \
790 self.fforward_button, self.rrewind_button, \
791 self.bookmarks_button:
793 button.set_longpress_enabled( self.config.getboolean("options", "dual_action_button") )
794 button.set_duration( self.config.getfloat("options", "dual_action_button_delay") )
796 def on_key_press(self, widget, event):
797 if platform.MAEMO:
798 if event.keyval == gtk.keysyms.Left: # seek back
799 self.do_seek( -1 * self.config.getint('options', 'seek_long') )
800 elif event.keyval == gtk.keysyms.Right: # seek forward
801 self.do_seek( self.config.getint('options', 'seek_long') )
802 elif event.keyval == gtk.keysyms.Return: # play/pause
803 self.on_btn_play_pause_clicked()
805 def on_player_stopped(self):
806 self.stop_progress_timer()
807 self.set_controls_sensitivity(False)
808 gtkutil.image(self.play_pause_button, 'media-playback-start.png')
810 def on_player_playing(self):
811 self.start_progress_timer()
812 gtkutil.image(self.play_pause_button, 'media-playback-pause.png')
813 self.set_controls_sensitivity(True)
814 if platform.FREMANTLE:
815 hildon.hildon_gtk_window_set_progress_indicator(\
816 self.__gui_root.main_window, False)
818 def on_player_eof(self):
819 play_mode = self.config.get("options", "play_mode")
820 if play_mode == "single":
821 if not self.config.getboolean("options", "stay_at_end"):
822 self.on_player_end_of_playlist(False)
823 elif play_mode == "random":
824 self.playlist.random()
825 elif play_mode == "repeat":
826 self.playlist.next(True)
827 else:
828 if self.playlist.end_of_playlist():
829 if not self.config.getboolean("options", "stay_at_end"):
830 self.playlist.next(False)
831 else:
832 self.playlist.next(False)
834 def on_player_new_track(self):
835 for widget in [self.title_label,self.artist_label,self.album_label]:
836 widget.set_markup('')
837 widget.hide()
839 self.cover_art.hide()
840 self.has_coverart = False
842 def on_player_new_metadata(self):
843 self.metadata = self.playlist.get_file_metadata()
844 self.set_metadata(self.metadata)
846 if not self.playlist.player.playing:
847 position = self.playlist.get_current_position()
848 estimated_length = self.metadata.get('length', 0)
849 self.set_progress_callback( position, estimated_length )
850 self.playlist.player.set_position_duration(position, 0)
852 def on_player_paused( self, position, duration ):
853 self.stop_progress_timer() # This should save some power
854 self.set_progress_callback( position, duration )
855 gtkutil.image(self.play_pause_button, 'media-playback-start.png')
857 def on_player_end_of_playlist(self, loop):
858 if not loop:
859 self.playlist.player.stop_end_of_playlist()
860 estimated_length = self.metadata.get('length', 0)
861 self.set_progress_callback( 0, estimated_length )
862 self.playlist.player.set_position_duration(0, 0)
864 def on_player_reset_playlist(self):
865 self.on_player_stopped()
866 self.on_player_new_track()
867 self.reset_progress()
869 def reset_progress(self):
870 self.progress.set_fraction(0)
871 self.set_progress_callback(0,0)
872 self.__gui_root.main_window.set_title("Panucci")
874 def set_progress_callback(self, time_elapsed, total_time):
875 """ times must be in nanoseconds """
876 time_string = "%s / %s" % ( util.convert_ns(time_elapsed),
877 util.convert_ns(total_time) )
878 self.progress.set_text( time_string )
879 fraction = float(time_elapsed) / float(total_time) if total_time else 0
880 self.progress.set_fraction( fraction )
882 def on_progressbar_changed(self, widget, event):
883 if ( not self.config.getboolean("options", "lock_progress") and
884 event.type == gtk.gdk.BUTTON_PRESS and event.button == 1 ):
885 new_fraction = event.x/float(widget.get_allocation().width)
886 resp = self.playlist.player.do_seek(percent=new_fraction)
887 if resp:
888 # Preemptively update the progressbar to make seeking smoother
889 self.set_progress_callback( *resp )
891 def on_btn_play_pause_clicked(self, widget=None):
892 self.playlist.player.play_pause_toggle()
894 def progress_timer_callback( self ):
895 if self.playlist.player.playing and not self.playlist.player.seeking:
896 pos_int, dur_int = self.playlist.player.get_position_duration()
897 # This prevents bogus values from being set while seeking
898 if ( pos_int > 10**9 ) and ( dur_int > 10**9 ):
899 self.set_progress_callback( pos_int, dur_int )
900 return True
902 def start_progress_timer( self ):
903 if self.progress_timer_id is not None:
904 self.stop_progress_timer()
906 self.progress_timer_id = gobject.timeout_add(
907 1000, self.progress_timer_callback )
909 def stop_progress_timer( self ):
910 if self.progress_timer_id is not None:
911 gobject.source_remove( self.progress_timer_id )
912 self.progress_timer_id = None
914 def get_coverart_size( self ):
915 if platform.MAEMO:
916 if self.__gui_root.fullscreen:
917 size = util.coverart_sizes['maemo fullscreen']
918 else:
919 size = util.coverart_sizes['maemo']
920 else:
921 size = util.coverart_sizes['normal']
923 return size, size
925 def set_coverart( self, pixbuf ):
926 self.cover_art.set_from_pixbuf(pixbuf)
927 self.cover_art.show()
928 self.has_coverart = True
930 def set_metadata( self, tag_message ):
931 tags = { 'title': self.title_label, 'artist': self.artist_label,
932 'album': self.album_label }
934 # set the coverart
935 if tag_message.has_key('image') and tag_message['image'] is not None:
936 value = tag_message['image']
938 pbl = gtk.gdk.PixbufLoader()
939 try:
940 pbl.write(value)
941 pbl.close()
942 pixbuf = pbl.get_pixbuf()
943 pixbuf = pixbuf.scale_simple(self.config.getint("options", "cover_height"),
944 self.config.getint("options", "cover_height"), gtk.gdk.INTERP_BILINEAR )
945 self.set_coverart(pixbuf)
946 except Exception, e:
947 self.__log.exception('Error setting coverart...')
949 # set the text metadata
950 for tag,value in tag_message.iteritems():
951 if tags.has_key(tag) and value is not None and value.strip():
952 if tag == "artist":
953 _str = '<big>' + cgi.escape(value) + '</big>'
954 elif tag == "album":
955 _str = cgi.escape(value)
956 elif tag == "title":
957 _str = '<b><big>' + cgi.escape(value) + '</big></b>'
958 if not platform.MAEMO:
959 value += ' - Panucci'
960 if platform.FREMANTLE and len(value) > 25:
961 value = value[:24] + '...'
962 self.__gui_root.main_window.set_title( value )
964 try:
965 tags[tag].set_markup(_str)
966 except TypeError, e:
967 self.__log.exception(str(e))
968 tags[tag].set_alignment( 0.5*int(not self.has_coverart), 0.5)
969 tags[tag].show()
971 def do_seek(self, seek_amount):
972 seek_amount = seek_amount*10**9
973 resp = None
974 if not self.config.getboolean("options", "seek_back") or self.playlist.start_of_playlist() or seek_amount > 0:
975 resp = self.playlist.player.do_seek(from_current=seek_amount)
976 else:
977 pos_int, dur_int = self.playlist.player.get_position_duration()
978 if pos_int + seek_amount >= 0:
979 resp = self.playlist.player.do_seek(from_current=seek_amount)
980 else:
981 self.playlist.prev()
982 pos_int, dur_int = self.playlist.player.get_position_duration()
983 resp = self.playlist.player.do_seek(from_beginning=dur_int+seek_amount)
984 if resp:
985 # Preemptively update the progressbar to make seeking smoother
986 self.set_progress_callback( *resp )