added fm transmitter dialog to gtk menu
[panucci.git] / src / panucci / gtkui / gtkmain.py
blobb6f9db0ce70532ed436968a574ff5b1eea86eea4
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.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 self.action_timer = gtk.Action('timer', _('Sleep Timer'), _('Start a timed shutdown'), None)
204 self.action_timer.connect('activate', self.create_timer_dialog)
205 self.action_fm = gtk.Action('fm', _('FM Transmitter'), _('Show FM transmitter dialog'), None)
206 self.action_fm.connect('activate', self.show_fm_transmitter)
207 # Settings menu
208 self.action_lock_progress = gtk.ToggleAction('lock_progress', 'Lock Progress Bar', None, None)
209 self.action_lock_progress.connect("activate", self.set_boolean_config_callback)
210 self.action_lock_progress.set_active(self.config.getboolean("options", "lock_progress"))
211 self.action_dual_action_button = gtk.ToggleAction('dual_action_button', 'Dual Action Button', None, None)
212 self.action_dual_action_button.connect("activate", self.set_boolean_config_callback)
213 self.action_dual_action_button.set_active(self.config.getboolean("options", "dual_action_button"))
214 self.action_stay_at_end = gtk.ToggleAction('stay_at_end', 'Stay at End', None, None)
215 self.action_stay_at_end.connect("activate", self.set_boolean_config_callback)
216 self.action_stay_at_end.set_active(self.config.getboolean("options", "stay_at_end"))
217 self.action_seek_back = gtk.ToggleAction('seek_back', 'Seek Back', None, None)
218 self.action_seek_back.connect("activate", self.set_boolean_config_callback)
219 self.action_seek_back.set_active(self.config.getboolean("options", "seek_back"))
220 self.action_scrolling_labels = gtk.ToggleAction('scrolling_labels', 'Scrolling Labels', None, None)
221 self.action_scrolling_labels.connect("activate", self.scrolling_labels_callback)
222 self.action_scrolling_labels.set_active(self.config.getboolean("options", "scrolling_labels"))
223 self.action_play_mode = gtk.Action('play_mode', 'Play Mode', None, None)
224 self.action_play_mode_all = gtk.RadioAction('all', 'All', None, None, 0)
225 self.action_play_mode_all.connect("activate", self.set_play_mode_callback)
226 self.action_play_mode_single = gtk.RadioAction('single', 'Single', None, None, 1)
227 self.action_play_mode_single.connect("activate", self.set_play_mode_callback)
228 self.action_play_mode_single.set_group(self.action_play_mode_all)
229 self.action_play_mode_random = gtk.RadioAction('random', 'Random', None, None, 1)
230 self.action_play_mode_random.connect("activate", self.set_play_mode_callback)
231 self.action_play_mode_random.set_group(self.action_play_mode_all)
232 self.action_play_mode_repeat = gtk.RadioAction('repeat', 'Repeat', None, None, 1)
233 self.action_play_mode_repeat.connect("activate", self.set_play_mode_callback)
234 self.action_play_mode_repeat.set_group(self.action_play_mode_all)
235 if self.config.get("options", "play_mode") == "single":
236 self.action_play_mode_single.set_active(True)
237 elif self.config.get("options", "play_mode") == "random":
238 self.action_play_mode_random.set_active(True)
239 elif self.config.get("options", "play_mode") == "repeat":
240 self.action_play_mode_repeat.set_active(True)
241 else:
242 self.action_play_mode_all.set_active(True)
243 # Help menu
244 self.action_about = gtk.Action('about', _('About'), _('Show application version'), gtk.STOCK_ABOUT)
245 self.action_about.connect('activate', self.about_callback)
247 def create_desktop_menu(self, menu_bar):
248 file_menu_item = gtk.MenuItem(_('File'))
249 file_menu = gtk.Menu()
250 file_menu.append(self.action_open.create_menu_item())
251 file_menu.append(self.action_open_dir.create_menu_item())
252 file_menu.append(self.action_save.create_menu_item())
253 file_menu.append(self.action_empty_playlist.create_menu_item())
254 file_menu.append(self.action_delete_bookmarks.create_menu_item())
255 file_menu.append(gtk.SeparatorMenuItem())
256 file_menu.append(self.action_quit.create_menu_item())
257 file_menu_item.set_submenu(file_menu)
258 menu_bar.append(file_menu_item)
260 tools_menu_item = gtk.MenuItem(_('Tools'))
261 tools_menu = gtk.Menu()
262 tools_menu.append(self.action_playlist.create_menu_item())
263 tools_menu.append(self.action_timer.create_menu_item())
264 #tools_menu.append(self.action_fm.create_menu_item())
265 tools_menu_item.set_submenu(tools_menu)
266 menu_bar.append(tools_menu_item)
268 settings_menu_item = gtk.MenuItem(_('Settings'))
269 settings_menu = gtk.Menu()
270 settings_menu.append(self.action_lock_progress.create_menu_item())
271 settings_menu.append(self.action_dual_action_button.create_menu_item())
272 settings_menu.append(self.action_stay_at_end.create_menu_item())
273 settings_menu.append(self.action_seek_back.create_menu_item())
274 settings_menu.append(self.action_scrolling_labels.create_menu_item())
275 play_mode_menu_item = self.action_play_mode.create_menu_item()
276 settings_menu.append(play_mode_menu_item)
277 play_mode_menu = gtk.Menu()
278 play_mode_menu_item.set_submenu(play_mode_menu)
279 play_mode_menu.append(self.action_play_mode_all.create_menu_item())
280 play_mode_menu.append(self.action_play_mode_single.create_menu_item())
281 play_mode_menu.append(self.action_play_mode_random.create_menu_item())
282 play_mode_menu.append(self.action_play_mode_repeat.create_menu_item())
283 settings_menu_item.set_submenu(settings_menu)
284 menu_bar.append(settings_menu_item)
286 help_menu_item = gtk.MenuItem(_('Help'))
287 help_menu = gtk.Menu()
288 help_menu.append(self.action_about.create_menu_item())
289 help_menu_item.set_submenu(help_menu)
290 menu_bar.append(help_menu_item)
292 def create_playlist_app_menu(self):
293 menu = hildon.AppMenu()
295 for action in (self.action_save,
296 self.action_delete_bookmarks):
297 b = gtk.Button()
298 action.connect_proxy(b)
299 menu.append(b)
301 menu.show_all()
302 return menu
304 def create_app_menu(self):
305 menu = hildon.AppMenu()
307 for action in (self.action_settings,
308 self.action_playlist,
309 self.action_open,
310 self.action_open_dir,
311 self.action_empty_playlist,
312 self.action_timer,
313 self.action_fm,
314 self.action_about):
315 b = gtk.Button()
316 action.connect_proxy(b)
317 menu.append(b)
319 menu.show_all()
320 return menu
322 def create_menu(self):
323 # the main menu
324 menu = gtk.Menu()
326 menu_open = gtk.ImageMenuItem(_('Add File'))
327 menu_open.set_image(
328 gtk.image_new_from_stock(gtk.STOCK_NEW, gtk.ICON_SIZE_MENU))
329 menu_open.connect("activate", self.open_file_callback)
330 menu.append(menu_open)
332 menu_open = gtk.ImageMenuItem(_('Add Folder'))
333 menu_open.set_image(
334 gtk.image_new_from_stock(gtk.STOCK_OPEN, gtk.ICON_SIZE_MENU))
335 menu_open.connect("activate", self.open_dir_callback)
336 menu.append(menu_open)
338 # the recent files menu
339 self.menu_recent = gtk.MenuItem(_('Open recent playlist'))
340 menu.append(self.menu_recent)
341 self.create_recent_files_menu()
343 menu.append(gtk.SeparatorMenuItem())
345 menu_save = gtk.ImageMenuItem(_('Save current playlist'))
346 menu_save.set_image(
347 gtk.image_new_from_stock(gtk.STOCK_SAVE_AS, gtk.ICON_SIZE_MENU))
348 menu_save.connect("activate", self.save_to_playlist_callback)
349 menu.append(menu_save)
351 menu_save = gtk.ImageMenuItem(_('Delete Playlist'))
352 menu_save.set_image(
353 gtk.image_new_from_stock(gtk.STOCK_DELETE, gtk.ICON_SIZE_MENU))
354 menu_save.connect("activate", self.empty_playlist_callback)
355 menu.append(menu_save)
357 menu_save = gtk.ImageMenuItem(_('Delete All Bookmarks'))
358 menu_save.set_image(
359 gtk.image_new_from_stock(gtk.STOCK_DELETE, gtk.ICON_SIZE_MENU))
360 menu_save.connect("activate", self.delete_all_bookmarks_callback)
361 menu.append(menu_save)
363 menu.append(gtk.SeparatorMenuItem())
365 # the settings sub-menu
366 menu_settings = gtk.MenuItem(_('Settings'))
367 menu.append(menu_settings)
369 menu_settings_sub = gtk.Menu()
370 menu_settings.set_submenu(menu_settings_sub)
372 menu_settings_enable_dual_action = gtk.CheckMenuItem(_('Enable dual-action buttons') )
373 menu_settings_enable_dual_action.connect('toggled', self.set_dual_action_button_callback)
374 menu_settings_enable_dual_action.set_active(self.config.getboolean("options", "dual_action_button"))
375 menu_settings_sub.append(menu_settings_enable_dual_action)
377 menu_settings_lock_progress = gtk.CheckMenuItem(_('Lock Progress Bar'))
378 menu_settings_lock_progress.connect('toggled', self.lock_progress_callback)
379 menu_settings_lock_progress.set_active(self.config.getboolean("options", "lock_progress"))
380 menu_settings_sub.append(menu_settings_lock_progress)
382 menu_about = gtk.ImageMenuItem(gtk.STOCK_ABOUT)
383 menu_about.connect("activate", self.about_callback)
384 menu.append(menu_about)
386 menu.append(gtk.SeparatorMenuItem())
388 menu_quit = gtk.ImageMenuItem(gtk.STOCK_QUIT)
389 menu_quit.connect("activate", self.destroy)
390 menu.append(menu_quit)
392 return menu
394 def create_timer_dialog(self,w):
395 response = widgets.IntDialog.get_int(_("Sleep Timer"), _("Shutdown time in minutes"), 5, 1)
396 if response[1]:
397 gobject.timeout_add(60000*response[0], self.timed_shutdown)
399 def timed_shutdown(self):
400 self.destroy( None )
401 return False
403 def create_recent_files_menu( self ):
404 max_files = self.config.getint("options", "max_recent_files")
405 self.recent_files = player.playlist.get_recent_files(max_files)
406 menu_recent_sub = gtk.Menu()
408 if len(self.recent_files) > 0:
409 for f in self.recent_files:
410 # don't include the temporary playlist in the file list
411 if f == panucci.PLAYLIST_FILE: continue
412 # don't include non-existant files
413 if not os.path.exists( f ): continue
414 filename, extension = os.path.splitext(os.path.basename(f))
415 menu_item = gtk.MenuItem( filename.replace('_', ' '))
416 menu_item.connect('activate', self.on_recent_file_activate, f)
417 menu_recent_sub.append(menu_item)
418 else:
419 menu_item = gtk.MenuItem(_('No recent files available.'))
420 menu_item.set_sensitive(False)
421 menu_recent_sub.append(menu_item)
423 self.menu_recent.set_submenu(menu_recent_sub)
425 def notify(self, message):
426 """ Sends a notification using pynotify, returns message """
427 if platform.DESKTOP and have_pynotify:
428 icon = util.find_data_file('panucci_64x64.png')
429 notification = pynotify.Notification(self.main_window.get_title(), message, icon)
430 notification.show()
431 elif platform.FREMANTLE:
432 hildon.hildon_banner_show_information(self.main_window, \
433 '', message)
434 elif platform.MAEMO:
435 # Note: This won't work if we're not in the gtk main loop
436 markup = '<b>%s</b>\n<small>%s</small>' % (self.main_window.get_title(), message)
437 hildon.hildon_banner_show_information_with_markup(self.main_window, None, markup)
439 def destroy(self, widget):
440 self.main_window.hide()
441 self.playlist.quit()
442 util.write_config(self.config)
443 gtk.main_quit()
445 def set_progress_indicator(self, loading_title=False):
446 if platform.FREMANTLE:
447 if loading_title:
448 self.main_window.set_title(_('Loading...'))
449 hildon.hildon_gtk_window_set_progress_indicator(self.main_window, \
450 True)
451 while gtk.events_pending():
452 gtk.main_iteration(False)
454 def show_main_window(self):
455 self.main_window.present()
457 def check_queue(self):
458 """ Makes sure the queue is saved if it has been modified
459 True means a new file can be opened
460 False means the user does not want to continue """
462 if not self.__ignore_queue_check and self.playlist.queue_modified:
463 response = gtkutil.dialog(
464 self.main_window, _('Save current playlist'),
465 _('Current playlist has been modified'),
466 _('Opening a new file will replace the current playlist. ') +
467 _('Do you want to save it before creating a new one?'),
468 affirmative_button=gtk.STOCK_SAVE,
469 negative_button=_('Discard changes'))
471 self.__log.debug('Response to "Save Queue?": %s', response)
473 if response is None:
474 return False
475 elif response:
476 return self.save_to_playlist_callback()
477 elif not response:
478 return True
479 else:
480 return False
481 else:
482 return True
484 def open_file_callback(self, widget=None):
485 # set __ingnore__queue_check because we already did the check
486 self.__ignore_queue_check = True
487 filename = gtkutil.get_file_from_filechooser(self)
488 if filename is not None:
489 self._play_file(filename)
491 self.__ignore_queue_check = False
493 def open_dir_callback(self, widget=None):
494 filename = gtkutil.get_file_from_filechooser(self, folder=True)
495 if filename is not None:
496 self._play_file(filename)
498 def save_to_playlist_callback(self, widget=None):
499 filename = gtkutil.get_file_from_filechooser(
500 self, save_file=True, save_to='playlist.m3u' )
502 if filename is None:
503 return False
505 if os.path.isfile(filename):
506 response = gtkutil.dialog( self.main_window, _('File already exists'),
507 _('File already exists'),
508 _('The file %s already exists. You can choose another name or '
509 'overwrite the existing file.') % os.path.basename(filename),
510 affirmative_button=gtk.STOCK_SAVE,
511 negative_button=_('Rename file'))
513 if response is None:
514 return None
516 elif response:
517 pass
518 elif not response:
519 return self.save_to_playlist_callback()
521 ext = util.detect_filetype(filename)
522 if not self.playlist.save_to_new_playlist(filename, ext):
523 self.notify(_('Error saving playlist...'))
524 return False
526 return True
528 def empty_playlist_callback(self, w):
529 self.playlist.reset_playlist()
530 self.__playlist_tab.treeview.get_model().clear()
532 def delete_all_bookmarks_callback(self, widget=None):
533 response = gtkutil.dialog(
534 self.main_window, _('Delete All Bookmarks'),
535 _('Would you like to delete all bookmarks?'),
536 _('By accepting all bookmarks in the database will be deleted.'),
537 negative_button=None)
539 self.__log.debug('Response to "Delete all bookmarks?": %s', response)
541 if response:
542 self.playlist.delete_all_bookmarks()
543 model = self.__playlist_tab.treeview.get_model()
545 for row in iter(model):
546 while model.iter_has_child(row.iter):
547 bkmk_iter = model.iter_children(row.iter)
548 model.remove(bkmk_iter)
550 def set_boolean_config_callback(self, w):
551 if w.get_active():
552 self.config.set("options", w.get_name(), "true")
553 else:
554 self.config.set("options", w.get_name(), "false")
556 def scrolling_labels_callback(self, w):
557 self.set_boolean_config_callback(w)
558 self.__player_tab.title_label.scrolling = w.get_active()
560 def set_play_mode_callback(self, w):
561 self.config.set("options", "play_mode", w.get_name())
563 def __get_fullscreen(self):
564 return self.__window_fullscreen
566 def __set_fullscreen(self, value):
567 if value != self.__window_fullscreen:
568 if value:
569 self.main_window.fullscreen()
570 else:
571 self.main_window.unfullscreen()
573 self.__window_fullscreen = value
574 self.playlist.send_metadata()
576 fullscreen = property( __get_fullscreen, __set_fullscreen )
578 def on_key_press(self, widget, event):
579 if platform.MAEMO:
580 if event.keyval == gtk.keysyms.F6:
581 self.fullscreen = not self.fullscreen
583 def on_recent_file_activate(self, widget, filepath):
584 self._play_file(filepath)
586 def on_file_queued(self, filepath, success, notify):
587 if notify:
588 filename = os.path.basename(filepath)
589 if success:
590 self.__log.info(
591 self.notify( '%s added successfully.' % filename ))
592 else:
593 self.__log.error(
594 self.notify( 'Error adding %s to the queue.' % filename))
596 def settings_callback(self, widget):
597 from panucci.gtkui.gtksettingsdialog import SettingsDialog
598 SettingsDialog(self)
600 def about_callback(self, widget):
601 if platform.FREMANTLE:
602 from panucci.gtkui.gtkaboutdialog import HeAboutDialog
603 HeAboutDialog.present(self.main_window, panucci.__version__)
604 else:
605 from panucci.gtkui.gtkaboutdialog import AboutDialog
606 AboutDialog(self.main_window, panucci.__version__)
608 def show_fm_transmitter(self, ws):
609 import osso
610 ctx = osso.Context('Panucci', '1.3.3.7', False)
611 plugin = osso.Plugin(ctx)
612 plugin.plugin_execute('libcpfmtx.so', True)
614 def _play_file(self, filename, pause_on_load=False):
615 self.playlist.load( os.path.abspath(filename) )
617 if self.playlist.is_empty:
618 return False
620 def handle_headset_button(self, event, button):
621 if event == 'ButtonPressed' and button == 'phone':
622 self.playlist.player.play_pause_toggle()
624 def __select_current_item( self ):
625 # Select the currently playing track in the playlist tab
626 # and switch to it (so we can edit bookmarks, etc.. there)
627 self.__playlist_tab.select_current_item()
628 self.playlist_window.show()
630 ##################################################
631 # PlayerTab
632 ##################################################
633 class PlayerTab(ObservableService, gtk.HBox):
634 """ The tab that holds the player elements """
636 signals = [ 'select-current-item-request', ]
638 def __init__(self, gui_root):
639 self.__log = logging.getLogger('panucci.panucci.PlayerTab')
640 self.__gui_root = gui_root
641 self.config = gui_root.config
642 self.playlist = gui_root.playlist
644 gtk.HBox.__init__(self)
645 ObservableService.__init__(self, self.signals, self.__log)
647 # Timers
648 self.progress_timer_id = None
650 self.recent_files = []
651 self.make_player_tab()
652 self.has_coverart = False
654 #settings.register( 'enable_dual_action_btn_changed',
655 # self.on_dual_action_setting_changed )
656 #settings.register( 'dual_action_button_delay_changed',
657 # self.on_dual_action_setting_changed )
658 #settings.register( 'scrolling_labels_changed', lambda v:
659 # setattr( self.title_label, 'scrolling', v ) )
661 self.playlist.player.register( 'stopped', self.on_player_stopped )
662 self.playlist.player.register( 'playing', self.on_player_playing )
663 self.playlist.player.register( 'paused', self.on_player_paused )
664 #self.playlist.player.register( 'eof', self.on_player_eof )
665 self.playlist.register( 'end-of-playlist',
666 self.on_player_end_of_playlist )
667 self.playlist.register( 'new-track-loaded',
668 self.on_player_new_track )
669 self.playlist.register( 'new-metadata-available',
670 self.on_player_new_metadata )
671 self.playlist.register( 'reset-playlist',
672 self.on_player_reset_playlist )
674 def make_player_tab(self):
675 main_vbox = gtk.VBox()
676 main_vbox.set_spacing(6)
677 # add a vbox to self
678 self.pack_start(main_vbox, True, True)
680 # a hbox to hold the cover art and metadata vbox
681 metadata_hbox = gtk.HBox()
682 metadata_hbox.set_spacing(6)
683 main_vbox.pack_start(metadata_hbox, True, False)
685 self.cover_art = gtk.Image()
686 metadata_hbox.pack_start( self.cover_art, False, False )
688 # vbox to hold metadata
689 metadata_vbox = gtk.VBox()
690 metadata_vbox.pack_start(gtk.Image(), True, True)
691 self.artist_label = gtk.Label('')
692 self.artist_label.set_ellipsize(pango.ELLIPSIZE_END)
693 metadata_vbox.pack_start(self.artist_label, False, False)
694 separator = gtk.Label("")
695 separator.set_size_request(-1, 10)
696 metadata_vbox.pack_start(separator, False, False)
697 self.album_label = gtk.Label('')
698 if platform.FREMANTLE:
699 hildon.hildon_helper_set_logical_font(self.album_label, 'SmallSystemFont')
700 hildon.hildon_helper_set_logical_color(self.album_label, gtk.RC_FG, gtk.STATE_NORMAL, 'SecondaryTextColor')
701 else:
702 self.album_label.modify_font(pango.FontDescription('normal 8'))
703 self.album_label.set_ellipsize(pango.ELLIPSIZE_END)
704 metadata_vbox.pack_start(self.album_label, False, False)
705 separator = gtk.Label("")
706 separator.set_size_request(-1, 10)
707 metadata_vbox.pack_start(separator, False, False)
708 self.title_label = widgets.ScrollingLabel('',
709 self.config.get("options", "scrolling_color"),
710 update_interval=100,
711 pixel_jump=1,
712 delay_btwn_scrolls=5000,
713 delay_halfway=3000)
714 self.title_label.scrolling = self.config.getboolean("options", "scrolling_labels")
715 metadata_vbox.pack_start(self.title_label, False, False)
716 metadata_vbox.pack_start(gtk.Image(), True, True)
717 metadata_hbox.pack_start( metadata_vbox, True, True )
719 progress_eventbox = gtk.EventBox()
720 progress_eventbox.set_events(gtk.gdk.BUTTON_PRESS_MASK)
721 progress_eventbox.connect(
722 'button-press-event', self.on_progressbar_changed )
723 self.progress = gtk.ProgressBar()
724 self.progress.set_size_request(-1, self.config.getint("options", "progress_height"))
725 progress_eventbox.add(self.progress)
726 main_vbox.pack_start( progress_eventbox, False, False )
728 # make the button box
729 buttonbox = gtk.HBox()
731 # A wrapper to help create DualActionButtons with the right settings
732 def create_da(widget, action, widget2=None, action2=None):
733 if platform.FREMANTLE:
734 widget2 = None
735 action2 = None
737 return widgets.DualActionButton(widget, action, self.config, widget2, action2)
739 self.rrewind_button = create_da(
740 gtkutil.generate_image('media-skip-backward.png'),
741 lambda: self.do_seek(-1*self.config.getint('options', 'seek_long')),
742 gtkutil.generate_image(gtk.STOCK_GOTO_FIRST, True),
743 self.playlist.prev)
744 buttonbox.add(self.rrewind_button)
746 self.rewind_button = create_da(
747 gtkutil.generate_image('media-seek-backward.png'),
748 lambda: self.do_seek(-1*self.config.getint('options', 'seek_short')))
749 buttonbox.add(self.rewind_button)
751 self.play_pause_button = gtk.Button('')
752 gtkutil.image(self.play_pause_button, 'media-playback-start.png')
753 self.play_pause_button.connect( 'clicked',
754 self.on_btn_play_pause_clicked )
755 self.play_pause_button.set_sensitive(False)
756 buttonbox.add(self.play_pause_button)
758 self.forward_button = create_da(
759 gtkutil.generate_image('media-seek-forward.png'),
760 lambda: self.do_seek(self.config.getint('options', 'seek_short')))
761 buttonbox.add(self.forward_button)
763 self.fforward_button = create_da(
764 gtkutil.generate_image('media-skip-forward.png'),
765 lambda: self.do_seek(self.config.getint('options', 'seek_long')),
766 gtkutil.generate_image(gtk.STOCK_GOTO_LAST, True),
767 self.playlist.next)
768 buttonbox.add(self.fforward_button)
770 self.bookmarks_button = create_da(
771 gtkutil.generate_image('bookmark-new.png'),
772 self.playlist.player.add_bookmark_at_current_position,
773 gtkutil.generate_image(gtk.STOCK_JUMP_TO, True),
774 lambda *args: self.notify('select-current-item-request'))
775 buttonbox.add(self.bookmarks_button)
776 self.set_controls_sensitivity(False)
778 if platform.FREMANTLE:
779 for child in buttonbox.get_children():
780 if isinstance(child, gtk.Button):
781 child.set_name('HildonButton-thumb')
783 buttonbox.set_size_request(-1, self.config.getint("options", "button_height"))
784 main_vbox.pack_start(buttonbox, False, False)
786 if platform.MAEMO:
787 self.__gui_root.main_window.connect( 'key-press-event',
788 self.on_key_press )
790 # Disable focus for all widgets, so we can use the cursor
791 # keys + enter to directly control our media player, which
792 # is handled by "key-press-event"
793 for w in (
794 self.rrewind_button, self.rewind_button,
795 self.play_pause_button, self.forward_button,
796 self.fforward_button, self.progress,
797 self.bookmarks_button, ):
798 w.unset_flags(gtk.CAN_FOCUS)
800 def set_controls_sensitivity(self, sensitive):
801 for button in self.forward_button, self.rewind_button, \
802 self.fforward_button, self.rrewind_button:
804 button.set_sensitive(sensitive)
806 # the play/pause button should always be available except
807 # for when the player starts without a file
808 self.play_pause_button.set_sensitive(True)
810 def on_dual_action_setting_changed( self, *args ):
811 for button in self.forward_button, self.rewind_button, \
812 self.fforward_button, self.rrewind_button, \
813 self.bookmarks_button:
815 button.set_longpress_enabled( self.config.getboolean("options", "dual_action_button") )
816 button.set_duration( self.config.getfloat("options", "dual_action_button_delay") )
818 def on_key_press(self, widget, event):
819 if platform.MAEMO:
820 if event.keyval == gtk.keysyms.Left: # seek back
821 self.do_seek( -1 * self.config.getint('options', 'seek_long') )
822 elif event.keyval == gtk.keysyms.Right: # seek forward
823 self.do_seek( self.config.getint('options', 'seek_long') )
824 elif event.keyval == gtk.keysyms.Return: # play/pause
825 self.on_btn_play_pause_clicked()
827 def on_player_stopped(self):
828 self.stop_progress_timer()
829 self.set_controls_sensitivity(False)
830 gtkutil.image(self.play_pause_button, 'media-playback-start.png')
832 def on_player_playing(self):
833 self.start_progress_timer()
834 gtkutil.image(self.play_pause_button, 'media-playback-pause.png')
835 self.set_controls_sensitivity(True)
836 if platform.FREMANTLE:
837 hildon.hildon_gtk_window_set_progress_indicator(\
838 self.__gui_root.main_window, False)
840 def on_player_new_track(self):
841 for widget in [self.title_label,self.artist_label,self.album_label]:
842 widget.set_markup('')
843 widget.hide()
845 self.cover_art.hide()
846 self.has_coverart = False
848 def on_player_new_metadata(self):
849 self.metadata = self.playlist.get_file_metadata()
850 self.set_metadata(self.metadata)
852 if not self.playlist.player.playing:
853 position = self.playlist.get_current_position()
854 estimated_length = self.metadata.get('length', 0)
855 self.set_progress_callback( position, estimated_length )
856 self.playlist.player.set_position_duration(position, 0)
858 def on_player_paused( self, position, duration ):
859 self.stop_progress_timer() # This should save some power
860 self.set_progress_callback( position, duration )
861 gtkutil.image(self.play_pause_button, 'media-playback-start.png')
863 def on_player_end_of_playlist(self, loop):
864 if not loop:
865 self.playlist.player.stop_end_of_playlist()
866 estimated_length = self.metadata.get('length', 0)
867 self.set_progress_callback( 0, estimated_length )
868 self.playlist.player.set_position_duration(0, 0)
870 def on_player_reset_playlist(self):
871 self.on_player_stopped()
872 self.on_player_new_track()
873 self.reset_progress()
875 def reset_progress(self):
876 self.progress.set_fraction(0)
877 self.set_progress_callback(0,0)
878 self.__gui_root.main_window.set_title("Panucci")
880 def set_progress_callback(self, time_elapsed, total_time):
881 """ times must be in nanoseconds """
882 time_string = "%s / %s" % ( util.convert_ns(time_elapsed),
883 util.convert_ns(total_time) )
884 self.progress.set_text( time_string )
885 fraction = float(time_elapsed) / float(total_time) if total_time else 0
886 self.progress.set_fraction( fraction )
888 def on_progressbar_changed(self, widget, event):
889 if ( not self.config.getboolean("options", "lock_progress") and
890 event.type == gtk.gdk.BUTTON_PRESS and event.button == 1 ):
891 new_fraction = event.x/float(widget.get_allocation().width)
892 resp = self.playlist.player.do_seek(percent=new_fraction)
893 if resp:
894 # Preemptively update the progressbar to make seeking smoother
895 self.set_progress_callback( *resp )
897 def on_btn_play_pause_clicked(self, widget=None):
898 self.playlist.player.play_pause_toggle()
900 def progress_timer_callback( self ):
901 if self.playlist.player.playing and not self.playlist.player.seeking:
902 pos_int, dur_int = self.playlist.player.get_position_duration()
903 # This prevents bogus values from being set while seeking
904 if ( pos_int > 10**9 ) and ( dur_int > 10**9 ):
905 self.set_progress_callback( pos_int, dur_int )
906 return True
908 def start_progress_timer( self ):
909 if self.progress_timer_id is not None:
910 self.stop_progress_timer()
912 self.progress_timer_id = gobject.timeout_add(
913 1000, self.progress_timer_callback )
915 def stop_progress_timer( self ):
916 if self.progress_timer_id is not None:
917 gobject.source_remove( self.progress_timer_id )
918 self.progress_timer_id = None
920 def get_coverart_size( self ):
921 if platform.MAEMO:
922 if self.__gui_root.fullscreen:
923 size = util.coverart_sizes['maemo fullscreen']
924 else:
925 size = util.coverart_sizes['maemo']
926 else:
927 size = util.coverart_sizes['normal']
929 return size, size
931 def set_coverart( self, pixbuf ):
932 self.cover_art.set_from_pixbuf(pixbuf)
933 self.cover_art.show()
934 self.has_coverart = True
936 def set_metadata( self, tag_message ):
937 tags = { 'title': self.title_label, 'artist': self.artist_label,
938 'album': self.album_label }
940 # set the coverart
941 if tag_message.has_key('image') and tag_message['image'] is not None:
942 value = tag_message['image']
944 pbl = gtk.gdk.PixbufLoader()
945 try:
946 pbl.write(value)
947 pbl.close()
948 pixbuf = pbl.get_pixbuf()
949 pixbuf = pixbuf.scale_simple(self.config.getint("options", "cover_height"),
950 self.config.getint("options", "cover_height"), gtk.gdk.INTERP_BILINEAR )
951 self.set_coverart(pixbuf)
952 except Exception, e:
953 self.__log.exception('Error setting coverart...')
955 # set the text metadata
956 for tag,value in tag_message.iteritems():
957 if tags.has_key(tag) and value is not None and value.strip():
958 if tag == "artist":
959 _str = '<big>' + cgi.escape(value) + '</big>'
960 elif tag == "album":
961 _str = cgi.escape(value)
962 elif tag == "title":
963 _str = '<b><big>' + cgi.escape(value) + '</big></b>'
964 if not platform.MAEMO:
965 value += ' - Panucci'
966 if platform.FREMANTLE and len(value) > 25:
967 value = value[:24] + '...'
968 self.__gui_root.main_window.set_title( value )
970 try:
971 tags[tag].set_markup(_str)
972 except TypeError, e:
973 self.__log.exception(str(e))
974 tags[tag].set_alignment( 0.5*int(not self.has_coverart), 0.5)
975 tags[tag].show()
977 def do_seek(self, seek_amount):
978 resp = self.playlist.do_seek(seek_amount*10**9)
979 if resp:
980 # Preemptively update the progressbar to make seeking smoother
981 self.set_progress_callback( *resp )