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