Maemo 5: Settings dialog (no close button, has parent)
[panucci.git] / src / panucci / gtkui / gtkmain.py
blob6093bcac180de0e9b73254d2559ac87d185911f0
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 sys
23 import os, os.path
24 import time
26 import gtk
27 import gobject
28 import pango
30 import cgi
31 import dbus
33 import panucci
35 from panucci import util
36 from panucci import platform
37 from panucci.gtkui import gtkwidgets as widgets
38 from panucci.gtkui import gtkplaylist
39 from panucci.gtkui import gtkutil
41 log = logging.getLogger('panucci.panucci')
43 try:
44 import pynotify
45 pynotify.init('Panucci')
46 have_pynotify = True
47 except:
48 have_pynotify = False
50 try:
51 import hildon
52 except:
53 if platform.MAEMO:
54 log.critical( 'Using GTK widgets, install "python2.5-hildon" '
55 'for this to work properly.' )
57 if platform.FREMANTLE:
58 # Workaround Maemo bug 6694 (Playback in Silent mode)
59 gobject.set_application_name('FMRadio')
61 from panucci.settings import settings
62 from panucci.player import player
63 from panucci.dbusinterface import interface
64 from panucci.services import ObservableService
66 gtk.icon_size_register('panucci-button', 32, 32)
68 ##################################################
69 # PanucciGUI
70 ##################################################
71 class PanucciGUI(object):
72 """ The object that holds the entire panucci gui """
74 def __init__(self, filename=None):
75 self.__log = logging.getLogger('panucci.panucci.PanucciGUI')
76 interface.register_gui(self)
77 self.config = settings.config
79 # Build the base ui (window and menubar)
80 if platform.MAEMO:
81 self.app = hildon.Program()
82 if platform.FREMANTLE:
83 window = hildon.StackableWindow()
84 else:
85 window = hildon.Window()
86 self.app.add_window(window)
87 else:
88 window = gtk.Window(gtk.WINDOW_TOPLEVEL)
90 self.main_window = window
91 window.set_title('Panucci')
92 self.window_icon = util.find_data_file('panucci.png')
93 if self.window_icon is not None:
94 window.set_icon_from_file( self.window_icon )
95 window.set_default_size(400, -1)
96 window.set_border_width(0)
97 window.connect("destroy", self.destroy)
99 # Add the tabs (they are private to prevent us from trying to do
100 # something like gui_root.player_tab.some_function() from inside
101 # playlist_tab or vice-versa)
102 self.__player_tab = PlayerTab(self)
103 self.__playlist_tab = gtkplaylist.PlaylistTab(self, player)
105 self.create_actions()
107 if platform.FREMANTLE:
108 self.playlist_window = hildon.StackableWindow()
109 self.playlist_window.set_app_menu(self.create_playlist_app_menu())
110 else:
111 self.playlist_window = gtk.Window(gtk.WINDOW_TOPLEVEL)
112 self.playlist_window.connect('delete-event', gtk.Widget.hide_on_delete)
113 self.playlist_window.set_title(_('Playlist'))
114 self.playlist_window.set_transient_for(self.main_window)
115 self.playlist_window.add(self.__playlist_tab)
117 if platform.MAEMO:
118 if platform.FREMANTLE:
119 window.set_app_menu(self.create_app_menu())
120 else:
121 window.set_menu(self.create_menu())
122 window.add(self.__player_tab)
123 else:
124 menu_vbox = gtk.VBox()
125 menu_vbox.set_spacing(0)
126 window.add(menu_vbox)
127 menu_bar = gtk.MenuBar()
128 self.create_desktop_menu(menu_bar)
129 menu_vbox.pack_start(menu_bar, False, False, 0)
130 menu_bar.show()
131 menu_vbox.pack_end(self.__player_tab, True, True, 6)
133 # Tie it all together!
134 self.__ignore_queue_check = False
135 self.__window_fullscreen = False
137 if platform.MAEMO and interface.headset_device:
138 # Enable play/pause with headset button
139 interface.headset_device.connect_to_signal('Condition', \
140 self.handle_headset_button)
141 system_bus = dbus.SystemBus()
143 # Monitor connection state of BT headset
144 # I haven't seen this option before "settings.play_on_headset"
145 PATH = '/org/freedesktop/Hal/devices/computer_logicaldev_input_1'
146 def handler_func(device_path):
147 if device_path == PATH and not player.playing:
148 player.play()
149 system_bus.add_signal_receiver(handler_func, 'DeviceAdded', \
150 'org.freedesktop.Hal.Manager', None, \
151 '/org/freedesktop/Hal/Manager')
152 # End Monitor connection state of BT headset
154 # Monitor BT headset buttons
155 def handle_bt_button(signal, button):
156 # See http://bugs.maemo.org/8283 for details
157 if signal == 'ButtonPressed':
158 if button == 'play-cd':
159 player.play_pause_toggle()
160 elif button == 'pause-cd':
161 player.pause()
162 elif button == 'next-song':
163 self.__player_tab.do_seek(settings.config.getint("options", "seek_short"))
164 elif button == 'previous-song':
165 self.__player_tab.do_seek(-1*settings.config.getint("options", "seek_short"))
167 system_bus.add_signal_receiver(handle_bt_button, 'Condition', \
168 'org.freedesktop.Hal.Device', None, PATH)
169 # End Monitor BT headset buttons
171 self.main_window.connect('key-press-event', self.on_key_press)
172 player.playlist.register( 'file_queued', self.on_file_queued )
174 player.playlist.register( 'playlist-to-be-overwritten',
175 self.check_queue )
176 self.__player_tab.register( 'select-current-item-request',
177 self.__select_current_item )
179 self.main_window.show_all()
181 # this should be done when the gui is ready
182 player.init(filepath=filename)
184 pos_int, dur_int = player.get_position_duration()
185 # This prevents bogus values from being set while seeking
186 if (pos_int > 10**9) and (dur_int > 10**9):
187 self.set_progress_callback(pos_int, dur_int)
189 gtk.main()
191 def create_actions(self):
192 # File menu
193 self.action_open = gtk.Action('open_file', _('Add File'), _('Open a file or playlist'), gtk.STOCK_NEW)
194 self.action_open.connect('activate', self.open_file_callback)
195 self.action_open_dir = gtk.Action('open_dir', _('Add Folder'), _('Open a directory'), gtk.STOCK_OPEN)
196 self.action_open_dir.connect('activate', self.open_dir_callback)
197 self.action_save = gtk.Action('save', _('Save Playlist'), _('Save current playlist to file'), gtk.STOCK_SAVE_AS)
198 self.action_save.connect('activate', self.save_to_playlist_callback)
199 self.action_empty_playlist = gtk.Action('empty_playlist', _('Clear Playlist'), _('Clear current playlist'), gtk.STOCK_DELETE)
200 self.action_empty_playlist.connect('activate', self.empty_playlist_callback)
201 self.action_delete_bookmarks = gtk.Action('delete_bookmarks', _('Delete All Bookmarks'), _('Deleting all bookmarks'), gtk.STOCK_DELETE)
202 self.action_delete_bookmarks.connect('activate', self.delete_all_bookmarks_callback)
203 self.action_quit = gtk.Action('quit', _('Quit'), _('Close Panucci'), gtk.STOCK_QUIT)
204 self.action_quit.connect('activate', self.destroy)
205 # Tools menu
206 self.action_playlist = gtk.Action('playlist', _('Playlist'), _('Open the current playlist'), None)
207 self.action_playlist.connect('activate', lambda a: self.playlist_window.show())
208 self.action_settings = gtk.Action('settings', _('Settings'), _('Open the settings dialog'), None)
209 self.action_settings.connect('activate', self.create_settings_dialog)
210 # Settings menu
211 self.action_lock_progress = gtk.ToggleAction('lock_progress', 'Lock Progress Bar', None, None)
212 self.action_lock_progress.connect("activate", self.set_boolean_config_callback)
213 self.action_lock_progress.set_active(settings.config.getboolean("options", "lock_progress"))
214 self.action_dual_action_button = gtk.ToggleAction('dual_action_button', 'Dual Action Button', None, None)
215 self.action_dual_action_button.connect("activate", self.set_boolean_config_callback)
216 self.action_dual_action_button.set_active(settings.config.getboolean("options", "dual_action_button"))
217 self.action_stay_at_end = gtk.ToggleAction('stay_at_end', 'Stay at End', None, None)
218 self.action_stay_at_end.connect("activate", self.set_boolean_config_callback)
219 self.action_stay_at_end.set_active(settings.config.getboolean("options", "stay_at_end"))
220 self.action_seek_back = gtk.ToggleAction('seek_back', 'Seek Back', None, None)
221 self.action_seek_back.connect("activate", self.set_boolean_config_callback)
222 self.action_seek_back.set_active(settings.config.getboolean("options", "seek_back"))
223 self.action_scrolling_labels = gtk.ToggleAction('scrolling_labels', 'Scrolling Labels', None, None)
224 self.action_scrolling_labels.connect("activate", self.scrolling_labels_callback)
225 self.action_scrolling_labels.set_active(settings.config.getboolean("options", "scrolling_labels"))
226 self.action_play_mode = gtk.Action('play_mode', 'Play Mode', None, None)
227 self.action_play_mode_all = gtk.RadioAction('all', 'All', None, None, 0)
228 self.action_play_mode_all.connect("activate", self.set_play_mode_callback)
229 self.action_play_mode_single = gtk.RadioAction('single', 'Single', None, None, 1)
230 self.action_play_mode_single.connect("activate", self.set_play_mode_callback)
231 self.action_play_mode_single.set_group(self.action_play_mode_all)
232 self.action_play_mode_random = gtk.RadioAction('random', 'Random', None, None, 1)
233 self.action_play_mode_random.connect("activate", self.set_play_mode_callback)
234 self.action_play_mode_random.set_group(self.action_play_mode_all)
235 self.action_play_mode_repeat = gtk.RadioAction('repeat', 'Repeat', None, None, 1)
236 self.action_play_mode_repeat.connect("activate", self.set_play_mode_callback)
237 self.action_play_mode_repeat.set_group(self.action_play_mode_all)
238 if settings.config.get("options", "play_mode") == "single":
239 self.action_play_mode_single.set_active(True)
240 elif settings.config.get("options", "play_mode") == "random":
241 self.action_play_mode_random.set_active(True)
242 elif settings.config.get("options", "play_mode") == "repeat":
243 self.action_play_mode_repeat.set_active(True)
244 else:
245 self.action_play_mode_all.set_active(True)
246 # Help menu
247 self.action_about = gtk.Action('about', _('About'), _('Show application version'), gtk.STOCK_ABOUT)
248 self.action_about.connect('activate', self.about_callback)
250 def create_desktop_menu(self, menu_bar):
251 file_menu_item = gtk.MenuItem(_('File'))
252 file_menu = gtk.Menu()
253 file_menu.append(self.action_open.create_menu_item())
254 file_menu.append(self.action_open_dir.create_menu_item())
255 file_menu.append(self.action_save.create_menu_item())
256 file_menu.append(self.action_empty_playlist.create_menu_item())
257 file_menu.append(self.action_delete_bookmarks.create_menu_item())
258 file_menu.append(gtk.SeparatorMenuItem())
259 file_menu.append(self.action_quit.create_menu_item())
260 file_menu_item.set_submenu(file_menu)
261 menu_bar.append(file_menu_item)
263 tools_menu_item = gtk.MenuItem(_('Tools'))
264 tools_menu = gtk.Menu()
265 tools_menu.append(self.action_playlist.create_menu_item())
266 #tools_menu.append(self.action_settings.create_menu_item())
267 tools_menu_item.set_submenu(tools_menu)
268 menu_bar.append(tools_menu_item)
270 settings_menu_item = gtk.MenuItem(_('Settings'))
271 settings_menu = gtk.Menu()
272 settings_menu.append(self.action_lock_progress.create_menu_item())
273 settings_menu.append(self.action_dual_action_button.create_menu_item())
274 settings_menu.append(self.action_stay_at_end.create_menu_item())
275 settings_menu.append(self.action_seek_back.create_menu_item())
276 settings_menu.append(self.action_scrolling_labels.create_menu_item())
277 play_mode_menu_item = self.action_play_mode.create_menu_item()
278 settings_menu.append(play_mode_menu_item)
279 play_mode_menu = gtk.Menu()
280 play_mode_menu_item.set_submenu(play_mode_menu)
281 play_mode_menu.append(self.action_play_mode_all.create_menu_item())
282 play_mode_menu.append(self.action_play_mode_single.create_menu_item())
283 play_mode_menu.append(self.action_play_mode_random.create_menu_item())
284 play_mode_menu.append(self.action_play_mode_repeat.create_menu_item())
285 settings_menu_item.set_submenu(settings_menu)
286 menu_bar.append(settings_menu_item)
288 help_menu_item = gtk.MenuItem(_('Help'))
289 help_menu = gtk.Menu()
290 help_menu.append(self.action_about.create_menu_item())
291 help_menu_item.set_submenu(help_menu)
292 menu_bar.append(help_menu_item)
294 def create_playlist_app_menu(self):
295 menu = hildon.AppMenu()
297 for action in (self.action_save,
298 self.action_empty_playlist,
299 self.action_delete_bookmarks):
300 b = gtk.Button()
301 action.connect_proxy(b)
302 menu.append(b)
304 menu.show_all()
305 return menu
307 def create_app_menu(self):
308 menu = hildon.AppMenu()
310 for action in (self.action_settings,
311 self.action_playlist,
312 self.action_open,
313 self.action_open_dir,
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(settings.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(settings.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_recent_files_menu( self ):
395 max_files = settings.config.getint("options", "max_recent_files")
396 self.recent_files = player.playlist.get_recent_files(max_files)
397 menu_recent_sub = gtk.Menu()
399 if len(self.recent_files) > 0:
400 for f in self.recent_files:
401 # don't include the temporary playlist in the file list
402 if f == panucci.PLAYLIST_FILE: continue
403 # don't include non-existant files
404 if not os.path.exists( f ): continue
405 filename, extension = os.path.splitext(os.path.basename(f))
406 menu_item = gtk.MenuItem( filename.replace('_', ' '))
407 menu_item.connect('activate', self.on_recent_file_activate, f)
408 menu_recent_sub.append(menu_item)
409 else:
410 menu_item = gtk.MenuItem(_('No recent files available.'))
411 menu_item.set_sensitive(False)
412 menu_recent_sub.append(menu_item)
414 self.menu_recent.set_submenu(menu_recent_sub)
416 def create_settings_dialog(self, w):
417 dialog = gtk.Dialog(_("Settings"),
418 self.main_window,
419 gtk.DIALOG_MODAL | gtk.DIALOG_DESTROY_WITH_PARENT)
421 # Maemo 5 has an implicit "close" button (tap outside of dialog)
422 if not platform.FREMANTLE:
423 dialog.add_button(gtk.STOCK_CLOSE, gtk.RESPONSE_ACCEPT)
425 table = gtk.Table(5, 2, True)
426 dialog.vbox.add(table)
427 b = gtk.CheckButton()
428 self.action_lock_progress.connect_proxy(b)
429 table.attach(b, 0, 1, 0, 1)
430 b = gtk.CheckButton()
431 self.action_dual_action_button.connect_proxy(b)
432 table.attach(b, 0, 1, 1, 2)
433 b = gtk.CheckButton()
434 self.action_stay_at_end.connect_proxy(b)
435 table.attach(b, 0, 1, 2, 3)
436 b = gtk.CheckButton()
437 self.action_seek_back.connect_proxy(b)
438 table.attach(b, 0, 1, 3, 4)
439 b = gtk.CheckButton()
440 self.action_scrolling_labels.connect_proxy(b)
441 table.attach(b, 0, 1, 4, 5)
442 label = gtk.Label(_("Play Mode"))
443 table.attach(label, 1, 2, 0, 1)
444 ra = gtk.RadioButton()
445 table.attach(ra, 1, 2, 1, 2)
446 rb = gtk.RadioButton()
447 rb.set_group(ra)
448 table.attach(rb, 1, 2, 2, 3)
449 rc = gtk.RadioButton()
450 rc.set_group(ra)
451 table.attach(rc, 1, 2, 3, 4)
452 rd = gtk.RadioButton()
453 rd.set_group(ra)
454 table.attach(rd, 1, 2, 4, 5)
455 if settings.config.get("options", "play_mode") == "single":
456 rb.set_active(True)
457 elif settings.config.get("options", "play_mode") == "random":
458 rc.set_active(True)
459 elif settings.config.get("options", "play_mode") == "repeat":
460 rd.set_active(True)
461 else:
462 ra.set_active(True)
463 self.action_play_mode_all.connect_proxy(ra)
464 self.action_play_mode_single.connect_proxy(rb)
465 self.action_play_mode_random.connect_proxy(rc)
466 self.action_play_mode_repeat.connect_proxy(rd)
467 dialog.show_all()
468 response = dialog.run()
469 dialog.destroy()
471 def notify(self, message):
472 """ Sends a notification using pynotify, returns message """
473 if platform.DESKTOP and have_pynotify:
474 icon = util.find_data_file('panucci_64x64.png')
475 notification = pynotify.Notification(self.main_window.get_title(), message, icon)
476 notification.show()
477 elif platform.FREMANTLE:
478 hildon.hildon_banner_show_information(self.main_window, \
479 '', message)
480 elif platform.MAEMO:
481 # Note: This won't work if we're not in the gtk main loop
482 markup = '<b>%s</b>\n<small>%s</small>' % (self.main_window.get_title(), message)
483 hildon.hildon_banner_show_information_with_markup(self.main_window, None, markup)
485 def destroy(self, widget):
486 self.main_window.hide()
487 player.quit()
488 gtk.main_quit()
490 def set_progress_indicator(self, loading_title=False):
491 if platform.FREMANTLE:
492 if loading_title:
493 self.main_window.set_title(_('Loading...'))
494 hildon.hildon_gtk_window_set_progress_indicator(self.main_window, \
495 True)
496 while gtk.events_pending():
497 gtk.main_iteration(False)
499 def show_main_window(self):
500 self.main_window.present()
502 def check_queue(self):
503 """ Makes sure the queue is saved if it has been modified
504 True means a new file can be opened
505 False means the user does not want to continue """
507 if not self.__ignore_queue_check and player.playlist.queue_modified:
508 response = gtkutil.dialog(
509 self.main_window, _('Save current playlist'),
510 _('Current playlist has been modified'),
511 _('Opening a new file will replace the current playlist. ') +
512 _('Do you want to save it before creating a new one?'),
513 affirmative_button=gtk.STOCK_SAVE,
514 negative_button=_('Discard changes'))
516 self.__log.debug('Response to "Save Queue?": %s', response)
518 if response is None:
519 return False
520 elif response:
521 return self.save_to_playlist_callback()
522 elif not response:
523 return True
524 else:
525 return False
526 else:
527 return True
529 def open_file_callback(self, widget=None):
530 # set __ingnore__queue_check because we already did the check
531 self.__ignore_queue_check = True
532 filename = gtkutil.get_file_from_filechooser(self)
533 if filename is not None:
534 self._play_file(filename)
536 self.__ignore_queue_check = False
538 def open_dir_callback(self, widget=None):
539 filename = gtkutil.get_file_from_filechooser(self, folder=True)
540 if filename is not None:
541 self._play_file(filename)
543 def save_to_playlist_callback(self, widget=None):
544 filename = gtkutil.get_file_from_filechooser(
545 self, save_file=True, save_to='playlist.m3u' )
547 if filename is None:
548 return False
550 if os.path.isfile(filename):
551 response = gtkutil.dialog( self.main_window, _('File already exists'),
552 _('File already exists'),
553 _('The file %s already exists. You can choose another name or '
554 'overwrite the existing file.') % os.path.basename(filename),
555 affirmative_button=gtk.STOCK_SAVE,
556 negative_button=_('Rename file'))
558 if response is None:
559 return None
561 elif response:
562 pass
563 elif not response:
564 return self.save_to_playlist_callback()
566 ext = util.detect_filetype(filename)
567 if not player.playlist.save_to_new_playlist(filename, ext):
568 self.notify(_('Error saving playlist...'))
569 return False
571 return True
573 def empty_playlist_callback(self, w):
574 player.playlist.reset_playlist()
575 self.__playlist_tab.treeview.get_model().clear()
577 def delete_all_bookmarks_callback(self, widget=None):
578 response = gtkutil.dialog(
579 self.main_window, _('Delete All Bookmarks'),
580 _('Would you like to delete all bookmarks?'),
581 _('By accepting all bookmarks in the database will be deleted.'),
582 negative_button=None)
584 self.__log.debug('Response to "Delete all bookmarks?": %s', response)
586 if response:
587 player.playlist.delete_all_bookmarks()
588 model = self.__playlist_tab.treeview.get_model()
590 for row in iter(model):
591 while model.iter_has_child(row.iter):
592 bkmk_iter = model.iter_children(row.iter)
593 model.remove(bkmk_iter)
595 def set_boolean_config_callback(self, w):
596 if w.get_active():
597 settings.config.set("options", w.get_name(), "true")
598 else:
599 settings.config.set("options", w.get_name(), "false")
601 def scrolling_labels_callback(self, w):
602 self.set_boolean_config_callback(w)
603 self.__player_tab.title_label.scrolling = w.get_active()
605 def set_play_mode_callback(self, w):
606 settings.config.set("options", "play_mode", w.get_name())
608 def __get_fullscreen(self):
609 return self.__window_fullscreen
611 def __set_fullscreen(self, value):
612 if value != self.__window_fullscreen:
613 if value:
614 self.main_window.fullscreen()
615 else:
616 self.main_window.unfullscreen()
618 self.__window_fullscreen = value
619 player.playlist.send_metadata()
621 fullscreen = property( __get_fullscreen, __set_fullscreen )
623 def on_key_press(self, widget, event):
624 if platform.MAEMO:
625 if event.keyval == gtk.keysyms.F6:
626 self.fullscreen = not self.fullscreen
628 def on_recent_file_activate(self, widget, filepath):
629 self._play_file(filepath)
631 def on_file_queued(self, filepath, success, notify):
632 if notify:
633 filename = os.path.basename(filepath)
634 if success:
635 self.__log.info(
636 self.notify( '%s added successfully.' % filename ))
637 else:
638 self.__log.error(
639 self.notify( 'Error adding %s to the queue.' % filename))
641 def about_callback(self, widget):
642 if platform.FREMANTLE:
643 from panucci.gtkui.gtkaboutdialog import HeAboutDialog
644 HeAboutDialog.present(self.main_window, panucci.__version__)
645 else:
646 from panucci.gtkui.gtkaboutdialog import AboutDialog
647 AboutDialog(self.main_window, panucci.__version__)
649 def _play_file(self, filename, pause_on_load=False):
650 player.playlist.load( os.path.abspath(filename) )
652 if player.playlist.is_empty:
653 return False
655 def handle_headset_button(self, event, button):
656 if event == 'ButtonPressed' and button == 'phone':
657 player.play_pause_toggle()
659 def __select_current_item( self ):
660 # Select the currently playing track in the playlist tab
661 # and switch to it (so we can edit bookmarks, etc.. there)
662 self.__playlist_tab.select_current_item()
663 self.playlist_window.show()
665 ##################################################
666 # PlayerTab
667 ##################################################
668 class PlayerTab(ObservableService, gtk.HBox):
669 """ The tab that holds the player elements """
671 signals = [ 'select-current-item-request', ]
673 def __init__(self, gui_root):
674 self.__log = logging.getLogger('panucci.panucci.PlayerTab')
675 self.__gui_root = gui_root
677 gtk.HBox.__init__(self)
678 ObservableService.__init__(self, self.signals, self.__log)
680 # Timers
681 self.progress_timer_id = None
683 self.recent_files = []
684 self.make_player_tab()
685 self.has_coverart = False
687 #settings.register( 'enable_dual_action_btn_changed',
688 # self.on_dual_action_setting_changed )
689 #settings.register( 'dual_action_button_delay_changed',
690 # self.on_dual_action_setting_changed )
691 #settings.register( 'scrolling_labels_changed', lambda v:
692 # setattr( self.title_label, 'scrolling', v ) )
694 player.register( 'stopped', self.on_player_stopped )
695 player.register( 'playing', self.on_player_playing )
696 player.register( 'paused', self.on_player_paused )
697 player.register( 'eof', self.on_player_eof )
698 player.playlist.register( 'end-of-playlist',
699 self.on_player_end_of_playlist )
700 player.playlist.register( 'new-track-loaded',
701 self.on_player_new_track )
702 player.playlist.register( 'new-metadata-available',
703 self.on_player_new_metadata )
704 player.playlist.register( 'reset-playlist',
705 self.on_player_reset_playlist )
707 def make_player_tab(self):
708 main_vbox = gtk.VBox()
709 main_vbox.set_spacing(6)
710 # add a vbox to self
711 self.pack_start(main_vbox, True, True)
713 # a hbox to hold the cover art and metadata vbox
714 metadata_hbox = gtk.HBox()
715 metadata_hbox.set_spacing(6)
716 main_vbox.pack_start(metadata_hbox, True, False)
718 self.cover_art = gtk.Image()
719 metadata_hbox.pack_start( self.cover_art, False, False )
721 # vbox to hold metadata
722 metadata_vbox = gtk.VBox()
723 metadata_vbox.pack_start(gtk.Image(), True, True)
724 self.artist_label = gtk.Label('')
725 self.artist_label.set_ellipsize(pango.ELLIPSIZE_END)
726 metadata_vbox.pack_start(self.artist_label, False, False)
727 self.album_label = gtk.Label('')
728 if platform.FREMANTLE:
729 hildon.hildon_helper_set_logical_font(self.album_label, 'SmallSystemFont')
730 hildon.hildon_helper_set_logical_color(self.album_label, gtk.RC_FG, gtk.STATE_NORMAL, 'SecondaryTextColor')
731 else:
732 self.album_label.modify_font(pango.FontDescription('normal 8'))
733 self.album_label.set_ellipsize(pango.ELLIPSIZE_END)
734 metadata_vbox.pack_start(self.album_label, False, False)
735 self.title_label = widgets.ScrollingLabel('',
736 settings.config.get("options", "scrolling_color"),
737 update_interval=100,
738 pixel_jump=1,
739 delay_btwn_scrolls=5000,
740 delay_halfway=3000)
741 self.title_label.scrolling = settings.config.getboolean("options", "scrolling_labels")
742 metadata_vbox.pack_start(self.title_label, False, False)
743 metadata_vbox.pack_start(gtk.Image(), True, True)
744 metadata_hbox.pack_start( metadata_vbox, True, True )
746 progress_eventbox = gtk.EventBox()
747 progress_eventbox.set_events(gtk.gdk.BUTTON_PRESS_MASK)
748 progress_eventbox.connect(
749 'button-press-event', self.on_progressbar_changed )
750 self.progress = gtk.ProgressBar()
751 # make the progress bar more "finger-friendly"
752 if platform.FREMANTLE:
753 self.progress.set_size_request(-1, 100)
754 elif platform.MAEMO:
755 self.progress.set_size_request(-1, 50)
756 progress_eventbox.add(self.progress)
757 main_vbox.pack_start( progress_eventbox, False, False )
759 # make the button box
760 buttonbox = gtk.HBox()
762 # A wrapper to help create DualActionButtons with the right settings
763 def create_da(widget, action, widget2=None, action2=None):
764 if platform.FREMANTLE:
765 widget2 = None
766 action2 = None
768 return widgets.DualActionButton(widget, action, settings.config, widget2, action2)
770 self.rrewind_button = create_da(
771 gtkutil.generate_image('media-skip-backward.png'),
772 lambda: self.do_seek(-1*settings.config.getint('options', 'seek_long')),
773 gtkutil.generate_image(gtk.STOCK_GOTO_FIRST, True),
774 player.playlist.prev)
775 buttonbox.add(self.rrewind_button)
777 self.rewind_button = create_da(
778 gtkutil.generate_image('media-seek-backward.png'),
779 lambda: self.do_seek(-1*settings.config.getint('options', 'seek_short')))
780 buttonbox.add(self.rewind_button)
782 self.play_pause_button = gtk.Button('')
783 gtkutil.image(self.play_pause_button, 'media-playback-start.png')
784 self.play_pause_button.connect( 'clicked',
785 self.on_btn_play_pause_clicked )
786 self.play_pause_button.set_sensitive(False)
787 buttonbox.add(self.play_pause_button)
789 self.forward_button = create_da(
790 gtkutil.generate_image('media-seek-forward.png'),
791 lambda: self.do_seek(settings.config.getint('options', 'seek_short')))
792 buttonbox.add(self.forward_button)
794 self.fforward_button = create_da(
795 gtkutil.generate_image('media-skip-forward.png'),
796 lambda: self.do_seek(settings.config.getint('options', 'seek_long')),
797 gtkutil.generate_image(gtk.STOCK_GOTO_LAST, True),
798 player.playlist.next)
799 buttonbox.add(self.fforward_button)
801 self.bookmarks_button = create_da(
802 gtkutil.generate_image('bookmark-new.png'),
803 player.add_bookmark_at_current_position,
804 gtkutil.generate_image(gtk.STOCK_JUMP_TO, True),
805 lambda *args: self.notify('select-current-item-request'))
806 buttonbox.add(self.bookmarks_button)
807 self.set_controls_sensitivity(False)
809 if platform.FREMANTLE:
810 for child in buttonbox.get_children():
811 if isinstance(child, gtk.Button):
812 child.set_name('HildonButton-thumb')
813 buttonbox.set_size_request(-1, 105)
815 main_vbox.pack_start(buttonbox, False, False)
817 if platform.MAEMO:
818 self.__gui_root.main_window.connect( 'key-press-event',
819 self.on_key_press )
821 # Disable focus for all widgets, so we can use the cursor
822 # keys + enter to directly control our media player, which
823 # is handled by "key-press-event"
824 for w in (
825 self.rrewind_button, self.rewind_button,
826 self.play_pause_button, self.forward_button,
827 self.fforward_button, self.progress,
828 self.bookmarks_button, ):
829 w.unset_flags(gtk.CAN_FOCUS)
831 def set_controls_sensitivity(self, sensitive):
832 for button in self.forward_button, self.rewind_button, \
833 self.fforward_button, self.rrewind_button:
835 button.set_sensitive(sensitive)
837 # the play/pause button should always be available except
838 # for when the player starts without a file
839 self.play_pause_button.set_sensitive(True)
841 def on_dual_action_setting_changed( self, *args ):
842 for button in self.forward_button, self.rewind_button, \
843 self.fforward_button, self.rrewind_button, \
844 self.bookmarks_button:
846 button.set_longpress_enabled( settings.config.getboolean("options", "enable_dual_action_btn") )
847 button.set_duration( settings.config.getfloat("options", "dual_action_button_delay") )
849 def on_key_press(self, widget, event):
850 if platform.MAEMO:
851 if event.keyval == gtk.keysyms.Left: # seek back
852 self.do_seek( -1 * settings.seek_long )
853 elif event.keyval == gtk.keysyms.Right: # seek forward
854 self.do_seek( settings.seek_long )
855 elif event.keyval == gtk.keysyms.Return: # play/pause
856 self.on_btn_play_pause_clicked()
858 def on_player_stopped(self):
859 self.stop_progress_timer()
860 self.set_controls_sensitivity(False)
861 gtkutil.image(self.play_pause_button, 'media-playback-start.png')
863 def on_player_playing(self):
864 self.start_progress_timer()
865 gtkutil.image(self.play_pause_button, 'media-playback-pause.png')
866 self.set_controls_sensitivity(True)
867 if platform.FREMANTLE:
868 hildon.hildon_gtk_window_set_progress_indicator(\
869 self.__gui_root.main_window, False)
871 def on_player_eof(self):
872 play_mode = settings.config.get("options", "play_mode")
873 if play_mode == "single":
874 if not settings.config.getboolean("options", "stay_at_end"):
875 self.on_player_end_of_playlist(False)
876 elif play_mode == "random":
877 player.playlist.random()
878 elif play_mode == "repeat":
879 player.playlist.next(True)
880 else:
881 if player.playlist.end_of_playlist():
882 if not settings.config.getboolean("options", "stay_at_end"):
883 player.playlist.next(False)
884 else:
885 player.playlist.next(False)
887 def on_player_new_track(self):
888 for widget in [self.title_label,self.artist_label,self.album_label]:
889 widget.set_markup('')
890 widget.hide()
892 self.cover_art.hide()
893 self.has_coverart = False
895 def on_player_new_metadata(self):
896 self.metadata = player.playlist.get_file_metadata()
897 self.set_metadata(self.metadata)
899 if not player.playing:
900 position = player.playlist.get_current_position()
901 estimated_length = self.metadata.get('length', 0)
902 self.set_progress_callback( position, estimated_length )
903 player.set_position_duration(position, 0)
905 def on_player_paused( self, position, duration ):
906 self.stop_progress_timer() # This should save some power
907 self.set_progress_callback( position, duration )
908 gtkutil.image(self.play_pause_button, 'media-playback-start.png')
910 def on_player_end_of_playlist(self, loop):
911 if not loop:
912 player.stop_end_of_playlist()
913 estimated_length = self.metadata.get('length', 0)
914 self.set_progress_callback( 0, estimated_length )
915 player.set_position_duration(0, 0)
917 def on_player_reset_playlist(self):
918 self.on_player_stopped()
919 self.on_player_new_track()
920 self.reset_progress()
922 def reset_progress(self):
923 self.progress.set_fraction(0)
924 self.set_progress_callback(0,0)
926 def set_progress_callback(self, time_elapsed, total_time):
927 """ times must be in nanoseconds """
928 time_string = "%s / %s" % ( util.convert_ns(time_elapsed),
929 util.convert_ns(total_time) )
930 self.progress.set_text( time_string )
931 fraction = float(time_elapsed) / float(total_time) if total_time else 0
932 self.progress.set_fraction( fraction )
934 def on_progressbar_changed(self, widget, event):
935 if ( not settings.config.getboolean("options", "lock_progress") and
936 event.type == gtk.gdk.BUTTON_PRESS and event.button == 1 ):
937 new_fraction = event.x/float(widget.get_allocation().width)
938 resp = player.do_seek(percent=new_fraction)
939 if resp:
940 # Preemptively update the progressbar to make seeking smoother
941 self.set_progress_callback( *resp )
943 def on_btn_play_pause_clicked(self, widget=None):
944 player.play_pause_toggle()
946 def progress_timer_callback( self ):
947 if player.playing and not player.seeking:
948 pos_int, dur_int = player.get_position_duration()
949 # This prevents bogus values from being set while seeking
950 if ( pos_int > 10**9 ) and ( dur_int > 10**9 ):
951 self.set_progress_callback( pos_int, dur_int )
952 return True
954 def start_progress_timer( self ):
955 if self.progress_timer_id is not None:
956 self.stop_progress_timer()
958 self.progress_timer_id = gobject.timeout_add(
959 1000, self.progress_timer_callback )
961 def stop_progress_timer( self ):
962 if self.progress_timer_id is not None:
963 gobject.source_remove( self.progress_timer_id )
964 self.progress_timer_id = None
966 def get_coverart_size( self ):
967 if platform.MAEMO:
968 if self.__gui_root.fullscreen:
969 size = util.coverart_sizes['maemo fullscreen']
970 else:
971 size = util.coverart_sizes['maemo']
972 else:
973 size = util.coverart_sizes['normal']
975 return size, size
977 def set_coverart( self, pixbuf ):
978 self.cover_art.set_from_pixbuf(pixbuf)
979 self.cover_art.show()
980 self.has_coverart = True
982 def set_metadata( self, tag_message ):
983 tags = { 'title': self.title_label, 'artist': self.artist_label,
984 'album': self.album_label }
986 # set the coverart
987 if tag_message.has_key('image') and tag_message['image'] is not None:
988 value = tag_message['image']
990 pbl = gtk.gdk.PixbufLoader()
991 try:
992 pbl.write(value)
993 pbl.close()
995 x, y = self.get_coverart_size()
996 pixbuf = pbl.get_pixbuf()
997 pixbuf = pixbuf.scale_simple( x, y, gtk.gdk.INTERP_BILINEAR )
998 self.set_coverart(pixbuf)
999 except Exception, e:
1000 self.__log.exception('Error setting coverart...')
1002 # set the text metadata
1003 for tag,value in tag_message.iteritems():
1004 if tags.has_key(tag) and value is not None and value.strip():
1005 try:
1006 tags[tag].set_markup('<big>'+cgi.escape(value)+'</big>')
1007 except TypeError, e:
1008 self.__log.exception(str(e))
1009 tags[tag].set_alignment( 0.5*int(not self.has_coverart), 0.5)
1010 tags[tag].show()
1012 if tag == 'title':
1013 # make the title bold
1014 tags[tag].set_markup('<b><big>'+cgi.escape(value)+'</big></b>')
1016 if not platform.MAEMO:
1017 value += ' - Panucci'
1019 if platform.FREMANTLE and len(value) > 25:
1020 value = value[:24] + '...'
1022 self.__gui_root.main_window.set_title( value )
1024 def do_seek(self, seek_amount):
1025 seek_amount = seek_amount*10**9
1026 resp = None
1027 if not settings.config.getboolean("options", "seek_back") or player.playlist.start_of_playlist() or seek_amount > 0:
1028 resp = player.do_seek(from_current=seek_amount)
1029 else:
1030 pos_int, dur_int = player.get_position_duration()
1031 if pos_int + seek_amount >= 0:
1032 resp = player.do_seek(from_current=seek_amount)
1033 else:
1034 player.playlist.prev()
1035 pos_int, dur_int = player.get_position_duration()
1036 resp = player.do_seek(from_beginning=dur_int+seek_amount)
1037 if resp:
1038 # Preemptively update the progressbar to make seeking smoother
1039 self.set_progress_callback( *resp )
1041 def run(filename=None):
1042 PanucciGUI(filename)
1043 gtk.main()
1045 if __name__ == '__main__':
1046 log.error( 'Use the "panucci" executable to run this program.' )
1047 log.error( 'Exiting...' )
1048 sys.exit(1)