moved write_config to destroy
[panucci.git] / src / panucci / gtkui / gtkmain.py
blobb69d95ad066cf2e0a6e18e8cd653eecbfefa1e77
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 settings.play_on_headset 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 if platform.FREMANTLE:
422 # Maemo 5 has an implicit "close" button (tap outside of dialog),
423 # but doesn't have instant-apply semantics, so add "Save" button
424 dialog.add_button(gtk.STOCK_SAVE, gtk.RESPONSE_APPLY)
425 else:
426 # GNOME's UI has instant-apply semantics -> add only Close button
427 dialog.add_button(gtk.STOCK_CLOSE, gtk.RESPONSE_CLOSE)
429 # List of (action, checkbutton) pairs to update when "Save" is clicked
430 actions_to_update = []
432 # Group for radio buttons
433 radio_button_group = []
435 if platform.FREMANTLE:
436 # Maemo 5: Create static connections, remember for updates
437 def check_button_factory(action):
438 b = hildon.CheckButton(gtk.HILDON_SIZE_FINGER_HEIGHT)
439 b.set_label(action.props.label)
440 b.set_active(action.get_active())
441 actions_to_update.append((action, b))
442 return b
443 def radio_button_factory(action):
444 b = hildon.GtkRadioButton(gtk.HILDON_SIZE_FINGER_HEIGHT)
445 b.set_mode(False)
446 if not radio_button_group:
447 radio_button_group.append(b)
448 else:
449 b.set_group(radio_button_group[0])
450 b.set_label(action.props.label)
451 b.set_active(action.get_active())
452 actions_to_update.append((action, b))
453 return b
454 else:
455 # GNOME/Desktop: Instant-apply by connecting check buttons
456 def check_button_factory(action):
457 b = gtk.CheckButton()
458 action.connect_proxy(b)
459 return b
460 def radio_button_factory(action):
461 global radio_button_group
462 b = gtk.RadioButton()
463 if not radio_button_group:
464 radio_button_group.append(b)
465 else:
466 b.set_group(radio_button_group[0])
467 action.connect_proxy(b)
468 b.set_active(action.get_active())
469 return b
471 vb = gtk.VBox()
473 if platform.FREMANTLE:
474 # On Maemo 5, the content could become bigger than the
475 # available screen space -> allow for vertical scrolling
476 pa = hildon.PannableArea()
477 pa.set_size_request_policy(hildon.SIZE_REQUEST_CHILDREN)
478 pa.add_with_viewport(vb)
479 dialog.vbox.add(pa)
480 else:
481 dialog.vbox.add(vb)
483 vb.pack_start(gtk.Frame('Main window'))
484 vb.pack_start(check_button_factory(self.action_scrolling_labels))
485 vb.pack_start(check_button_factory(self.action_lock_progress))
486 vb.pack_start(check_button_factory(self.action_dual_action_button))
488 vb.pack_start(gtk.Label('')) # Used as a spacer
490 vb.pack_start(gtk.Frame('Playback'))
491 vb.pack_start(check_button_factory(self.action_stay_at_end))
492 vb.pack_start(check_button_factory(self.action_seek_back))
494 vb.pack_start(gtk.Label('')) # Used as a spacer
496 vb.pack_start(gtk.Frame('Play mode'))
497 hb = gtk.HBox(homogeneous=True)
498 vb.pack_start(hb)
500 for action in (self.action_play_mode_all,
501 self.action_play_mode_single,
502 self.action_play_mode_random,
503 self.action_play_mode_repeat):
504 hb.pack_start(radio_button_factory(action))
506 dialog.show_all()
507 response = dialog.run()
509 if response == gtk.RESPONSE_APPLY:
510 # On Maemo 5, if the user picked "Save" we need
511 # to copy the state of the buttons into our actions
512 for action, button in actions_to_update:
513 action.set_active(button.get_active())
515 dialog.destroy()
517 def notify(self, message):
518 """ Sends a notification using pynotify, returns message """
519 if platform.DESKTOP and have_pynotify:
520 icon = util.find_data_file('panucci_64x64.png')
521 notification = pynotify.Notification(self.main_window.get_title(), message, icon)
522 notification.show()
523 elif platform.FREMANTLE:
524 hildon.hildon_banner_show_information(self.main_window, \
525 '', message)
526 elif platform.MAEMO:
527 # Note: This won't work if we're not in the gtk main loop
528 markup = '<b>%s</b>\n<small>%s</small>' % (self.main_window.get_title(), message)
529 hildon.hildon_banner_show_information_with_markup(self.main_window, None, markup)
531 def destroy(self, widget):
532 self.main_window.hide()
533 player.quit()
534 self.write_config()
535 gtk.main_quit()
537 def set_progress_indicator(self, loading_title=False):
538 if platform.FREMANTLE:
539 if loading_title:
540 self.main_window.set_title(_('Loading...'))
541 hildon.hildon_gtk_window_set_progress_indicator(self.main_window, \
542 True)
543 while gtk.events_pending():
544 gtk.main_iteration(False)
546 def show_main_window(self):
547 self.main_window.present()
549 def check_queue(self):
550 """ Makes sure the queue is saved if it has been modified
551 True means a new file can be opened
552 False means the user does not want to continue """
554 if not self.__ignore_queue_check and player.playlist.queue_modified:
555 response = gtkutil.dialog(
556 self.main_window, _('Save current playlist'),
557 _('Current playlist has been modified'),
558 _('Opening a new file will replace the current playlist. ') +
559 _('Do you want to save it before creating a new one?'),
560 affirmative_button=gtk.STOCK_SAVE,
561 negative_button=_('Discard changes'))
563 self.__log.debug('Response to "Save Queue?": %s', response)
565 if response is None:
566 return False
567 elif response:
568 return self.save_to_playlist_callback()
569 elif not response:
570 return True
571 else:
572 return False
573 else:
574 return True
576 def open_file_callback(self, widget=None):
577 # set __ingnore__queue_check because we already did the check
578 self.__ignore_queue_check = True
579 filename = gtkutil.get_file_from_filechooser(self)
580 if filename is not None:
581 self._play_file(filename)
583 self.__ignore_queue_check = False
585 def open_dir_callback(self, widget=None):
586 filename = gtkutil.get_file_from_filechooser(self, folder=True)
587 if filename is not None:
588 self._play_file(filename)
590 def save_to_playlist_callback(self, widget=None):
591 filename = gtkutil.get_file_from_filechooser(
592 self, save_file=True, save_to='playlist.m3u' )
594 if filename is None:
595 return False
597 if os.path.isfile(filename):
598 response = gtkutil.dialog( self.main_window, _('File already exists'),
599 _('File already exists'),
600 _('The file %s already exists. You can choose another name or '
601 'overwrite the existing file.') % os.path.basename(filename),
602 affirmative_button=gtk.STOCK_SAVE,
603 negative_button=_('Rename file'))
605 if response is None:
606 return None
608 elif response:
609 pass
610 elif not response:
611 return self.save_to_playlist_callback()
613 ext = util.detect_filetype(filename)
614 if not player.playlist.save_to_new_playlist(filename, ext):
615 self.notify(_('Error saving playlist...'))
616 return False
618 return True
620 def empty_playlist_callback(self, w):
621 player.playlist.reset_playlist()
622 self.__playlist_tab.treeview.get_model().clear()
624 def delete_all_bookmarks_callback(self, widget=None):
625 response = gtkutil.dialog(
626 self.main_window, _('Delete All Bookmarks'),
627 _('Would you like to delete all bookmarks?'),
628 _('By accepting all bookmarks in the database will be deleted.'),
629 negative_button=None)
631 self.__log.debug('Response to "Delete all bookmarks?": %s', response)
633 if response:
634 player.playlist.delete_all_bookmarks()
635 model = self.__playlist_tab.treeview.get_model()
637 for row in iter(model):
638 while model.iter_has_child(row.iter):
639 bkmk_iter = model.iter_children(row.iter)
640 model.remove(bkmk_iter)
642 def set_boolean_config_callback(self, w):
643 if w.get_active():
644 settings.config.set("options", w.get_name(), "true")
645 else:
646 settings.config.set("options", w.get_name(), "false")
648 def scrolling_labels_callback(self, w):
649 self.set_boolean_config_callback(w)
650 self.__player_tab.title_label.scrolling = w.get_active()
652 def set_play_mode_callback(self, w):
653 settings.config.set("options", "play_mode", w.get_name())
655 def write_config(self):
656 _file = open(os.path.expanduser("~/.config/panucci/panucci-noedit.conf"), "w")
657 settings.config.write(_file)
658 _file.close()
660 def __get_fullscreen(self):
661 return self.__window_fullscreen
663 def __set_fullscreen(self, value):
664 if value != self.__window_fullscreen:
665 if value:
666 self.main_window.fullscreen()
667 else:
668 self.main_window.unfullscreen()
670 self.__window_fullscreen = value
671 player.playlist.send_metadata()
673 fullscreen = property( __get_fullscreen, __set_fullscreen )
675 def on_key_press(self, widget, event):
676 if platform.MAEMO:
677 if event.keyval == gtk.keysyms.F6:
678 self.fullscreen = not self.fullscreen
680 def on_recent_file_activate(self, widget, filepath):
681 self._play_file(filepath)
683 def on_file_queued(self, filepath, success, notify):
684 if notify:
685 filename = os.path.basename(filepath)
686 if success:
687 self.__log.info(
688 self.notify( '%s added successfully.' % filename ))
689 else:
690 self.__log.error(
691 self.notify( 'Error adding %s to the queue.' % filename))
693 def about_callback(self, widget):
694 if platform.FREMANTLE:
695 from panucci.gtkui.gtkaboutdialog import HeAboutDialog
696 HeAboutDialog.present(self.main_window, panucci.__version__)
697 else:
698 from panucci.gtkui.gtkaboutdialog import AboutDialog
699 AboutDialog(self.main_window, panucci.__version__)
701 def _play_file(self, filename, pause_on_load=False):
702 player.playlist.load( os.path.abspath(filename) )
704 if player.playlist.is_empty:
705 return False
707 def handle_headset_button(self, event, button):
708 if event == 'ButtonPressed' and button == 'phone':
709 player.play_pause_toggle()
711 def __select_current_item( self ):
712 # Select the currently playing track in the playlist tab
713 # and switch to it (so we can edit bookmarks, etc.. there)
714 self.__playlist_tab.select_current_item()
715 self.playlist_window.show()
717 ##################################################
718 # PlayerTab
719 ##################################################
720 class PlayerTab(ObservableService, gtk.HBox):
721 """ The tab that holds the player elements """
723 signals = [ 'select-current-item-request', ]
725 def __init__(self, gui_root):
726 self.__log = logging.getLogger('panucci.panucci.PlayerTab')
727 self.__gui_root = gui_root
729 gtk.HBox.__init__(self)
730 ObservableService.__init__(self, self.signals, self.__log)
732 # Timers
733 self.progress_timer_id = None
735 self.recent_files = []
736 self.make_player_tab()
737 self.has_coverart = False
739 #settings.register( 'enable_dual_action_btn_changed',
740 # self.on_dual_action_setting_changed )
741 #settings.register( 'dual_action_button_delay_changed',
742 # self.on_dual_action_setting_changed )
743 #settings.register( 'scrolling_labels_changed', lambda v:
744 # setattr( self.title_label, 'scrolling', v ) )
746 player.register( 'stopped', self.on_player_stopped )
747 player.register( 'playing', self.on_player_playing )
748 player.register( 'paused', self.on_player_paused )
749 player.register( 'eof', self.on_player_eof )
750 player.playlist.register( 'end-of-playlist',
751 self.on_player_end_of_playlist )
752 player.playlist.register( 'new-track-loaded',
753 self.on_player_new_track )
754 player.playlist.register( 'new-metadata-available',
755 self.on_player_new_metadata )
756 player.playlist.register( 'reset-playlist',
757 self.on_player_reset_playlist )
759 def make_player_tab(self):
760 main_vbox = gtk.VBox()
761 main_vbox.set_spacing(6)
762 # add a vbox to self
763 self.pack_start(main_vbox, True, True)
765 # a hbox to hold the cover art and metadata vbox
766 metadata_hbox = gtk.HBox()
767 metadata_hbox.set_spacing(6)
768 main_vbox.pack_start(metadata_hbox, True, False)
770 self.cover_art = gtk.Image()
771 metadata_hbox.pack_start( self.cover_art, False, False )
773 # vbox to hold metadata
774 metadata_vbox = gtk.VBox()
775 metadata_vbox.pack_start(gtk.Image(), True, True)
776 self.artist_label = gtk.Label('')
777 self.artist_label.set_ellipsize(pango.ELLIPSIZE_END)
778 metadata_vbox.pack_start(self.artist_label, False, False)
779 separator = gtk.Label("")
780 separator.set_size_request(-1, 10)
781 metadata_vbox.pack_start(separator, False, False)
782 self.album_label = gtk.Label('')
783 if platform.FREMANTLE:
784 hildon.hildon_helper_set_logical_font(self.album_label, 'SmallSystemFont')
785 hildon.hildon_helper_set_logical_color(self.album_label, gtk.RC_FG, gtk.STATE_NORMAL, 'SecondaryTextColor')
786 else:
787 self.album_label.modify_font(pango.FontDescription('normal 8'))
788 self.album_label.set_ellipsize(pango.ELLIPSIZE_END)
789 metadata_vbox.pack_start(self.album_label, False, False)
790 separator = gtk.Label("")
791 separator.set_size_request(-1, 10)
792 metadata_vbox.pack_start(separator, False, False)
793 self.title_label = widgets.ScrollingLabel('',
794 settings.config.get("options", "scrolling_color"),
795 update_interval=100,
796 pixel_jump=1,
797 delay_btwn_scrolls=5000,
798 delay_halfway=3000)
799 self.title_label.scrolling = settings.config.getboolean("options", "scrolling_labels")
800 metadata_vbox.pack_start(self.title_label, False, False)
801 metadata_vbox.pack_start(gtk.Image(), True, True)
802 metadata_hbox.pack_start( metadata_vbox, True, True )
804 progress_eventbox = gtk.EventBox()
805 progress_eventbox.set_events(gtk.gdk.BUTTON_PRESS_MASK)
806 progress_eventbox.connect(
807 'button-press-event', self.on_progressbar_changed )
808 self.progress = gtk.ProgressBar()
809 self.progress.set_size_request(-1, settings.config.getint("options", "progress_height"))
810 progress_eventbox.add(self.progress)
811 main_vbox.pack_start( progress_eventbox, False, False )
813 # make the button box
814 buttonbox = gtk.HBox()
816 # A wrapper to help create DualActionButtons with the right settings
817 def create_da(widget, action, widget2=None, action2=None):
818 if platform.FREMANTLE:
819 widget2 = None
820 action2 = None
822 return widgets.DualActionButton(widget, action, settings.config, widget2, action2)
824 self.rrewind_button = create_da(
825 gtkutil.generate_image('media-skip-backward.png'),
826 lambda: self.do_seek(-1*settings.config.getint('options', 'seek_long')),
827 gtkutil.generate_image(gtk.STOCK_GOTO_FIRST, True),
828 player.playlist.prev)
829 buttonbox.add(self.rrewind_button)
831 self.rewind_button = create_da(
832 gtkutil.generate_image('media-seek-backward.png'),
833 lambda: self.do_seek(-1*settings.config.getint('options', 'seek_short')))
834 buttonbox.add(self.rewind_button)
836 self.play_pause_button = gtk.Button('')
837 gtkutil.image(self.play_pause_button, 'media-playback-start.png')
838 self.play_pause_button.connect( 'clicked',
839 self.on_btn_play_pause_clicked )
840 self.play_pause_button.set_sensitive(False)
841 buttonbox.add(self.play_pause_button)
843 self.forward_button = create_da(
844 gtkutil.generate_image('media-seek-forward.png'),
845 lambda: self.do_seek(settings.config.getint('options', 'seek_short')))
846 buttonbox.add(self.forward_button)
848 self.fforward_button = create_da(
849 gtkutil.generate_image('media-skip-forward.png'),
850 lambda: self.do_seek(settings.config.getint('options', 'seek_long')),
851 gtkutil.generate_image(gtk.STOCK_GOTO_LAST, True),
852 player.playlist.next)
853 buttonbox.add(self.fforward_button)
855 self.bookmarks_button = create_da(
856 gtkutil.generate_image('bookmark-new.png'),
857 player.add_bookmark_at_current_position,
858 gtkutil.generate_image(gtk.STOCK_JUMP_TO, True),
859 lambda *args: self.notify('select-current-item-request'))
860 buttonbox.add(self.bookmarks_button)
861 self.set_controls_sensitivity(False)
863 if platform.FREMANTLE:
864 for child in buttonbox.get_children():
865 if isinstance(child, gtk.Button):
866 child.set_name('HildonButton-thumb')
868 buttonbox.set_size_request(-1, settings.config.getint("options", "button_height"))
869 main_vbox.pack_start(buttonbox, False, False)
871 if platform.MAEMO:
872 self.__gui_root.main_window.connect( 'key-press-event',
873 self.on_key_press )
875 # Disable focus for all widgets, so we can use the cursor
876 # keys + enter to directly control our media player, which
877 # is handled by "key-press-event"
878 for w in (
879 self.rrewind_button, self.rewind_button,
880 self.play_pause_button, self.forward_button,
881 self.fforward_button, self.progress,
882 self.bookmarks_button, ):
883 w.unset_flags(gtk.CAN_FOCUS)
885 def set_controls_sensitivity(self, sensitive):
886 for button in self.forward_button, self.rewind_button, \
887 self.fforward_button, self.rrewind_button:
889 button.set_sensitive(sensitive)
891 # the play/pause button should always be available except
892 # for when the player starts without a file
893 self.play_pause_button.set_sensitive(True)
895 def on_dual_action_setting_changed( self, *args ):
896 for button in self.forward_button, self.rewind_button, \
897 self.fforward_button, self.rrewind_button, \
898 self.bookmarks_button:
900 button.set_longpress_enabled( settings.config.getboolean("options", "dual_action_button") )
901 button.set_duration( settings.config.getfloat("options", "dual_action_button_delay") )
903 def on_key_press(self, widget, event):
904 if platform.MAEMO:
905 if event.keyval == gtk.keysyms.Left: # seek back
906 self.do_seek( -1 * settings.seek_long )
907 elif event.keyval == gtk.keysyms.Right: # seek forward
908 self.do_seek( settings.seek_long )
909 elif event.keyval == gtk.keysyms.Return: # play/pause
910 self.on_btn_play_pause_clicked()
912 def on_player_stopped(self):
913 self.stop_progress_timer()
914 self.set_controls_sensitivity(False)
915 gtkutil.image(self.play_pause_button, 'media-playback-start.png')
917 def on_player_playing(self):
918 self.start_progress_timer()
919 gtkutil.image(self.play_pause_button, 'media-playback-pause.png')
920 self.set_controls_sensitivity(True)
921 if platform.FREMANTLE:
922 hildon.hildon_gtk_window_set_progress_indicator(\
923 self.__gui_root.main_window, False)
925 def on_player_eof(self):
926 play_mode = settings.config.get("options", "play_mode")
927 if play_mode == "single":
928 if not settings.config.getboolean("options", "stay_at_end"):
929 self.on_player_end_of_playlist(False)
930 elif play_mode == "random":
931 player.playlist.random()
932 elif play_mode == "repeat":
933 player.playlist.next(True)
934 else:
935 if player.playlist.end_of_playlist():
936 if not settings.config.getboolean("options", "stay_at_end"):
937 player.playlist.next(False)
938 else:
939 player.playlist.next(False)
941 def on_player_new_track(self):
942 for widget in [self.title_label,self.artist_label,self.album_label]:
943 widget.set_markup('')
944 widget.hide()
946 self.cover_art.hide()
947 self.has_coverart = False
949 def on_player_new_metadata(self):
950 self.metadata = player.playlist.get_file_metadata()
951 self.set_metadata(self.metadata)
953 if not player.playing:
954 position = player.playlist.get_current_position()
955 estimated_length = self.metadata.get('length', 0)
956 self.set_progress_callback( position, estimated_length )
957 player.set_position_duration(position, 0)
959 def on_player_paused( self, position, duration ):
960 self.stop_progress_timer() # This should save some power
961 self.set_progress_callback( position, duration )
962 gtkutil.image(self.play_pause_button, 'media-playback-start.png')
964 def on_player_end_of_playlist(self, loop):
965 if not loop:
966 player.stop_end_of_playlist()
967 estimated_length = self.metadata.get('length', 0)
968 self.set_progress_callback( 0, estimated_length )
969 player.set_position_duration(0, 0)
971 def on_player_reset_playlist(self):
972 self.on_player_stopped()
973 self.on_player_new_track()
974 self.reset_progress()
976 def reset_progress(self):
977 self.progress.set_fraction(0)
978 self.set_progress_callback(0,0)
979 self.__gui_root.main_window.set_title("Panucci")
981 def set_progress_callback(self, time_elapsed, total_time):
982 """ times must be in nanoseconds """
983 time_string = "%s / %s" % ( util.convert_ns(time_elapsed),
984 util.convert_ns(total_time) )
985 self.progress.set_text( time_string )
986 fraction = float(time_elapsed) / float(total_time) if total_time else 0
987 self.progress.set_fraction( fraction )
989 def on_progressbar_changed(self, widget, event):
990 if ( not settings.config.getboolean("options", "lock_progress") and
991 event.type == gtk.gdk.BUTTON_PRESS and event.button == 1 ):
992 new_fraction = event.x/float(widget.get_allocation().width)
993 resp = player.do_seek(percent=new_fraction)
994 if resp:
995 # Preemptively update the progressbar to make seeking smoother
996 self.set_progress_callback( *resp )
998 def on_btn_play_pause_clicked(self, widget=None):
999 player.play_pause_toggle()
1001 def progress_timer_callback( self ):
1002 if player.playing and not player.seeking:
1003 pos_int, dur_int = player.get_position_duration()
1004 # This prevents bogus values from being set while seeking
1005 if ( pos_int > 10**9 ) and ( dur_int > 10**9 ):
1006 self.set_progress_callback( pos_int, dur_int )
1007 return True
1009 def start_progress_timer( self ):
1010 if self.progress_timer_id is not None:
1011 self.stop_progress_timer()
1013 self.progress_timer_id = gobject.timeout_add(
1014 1000, self.progress_timer_callback )
1016 def stop_progress_timer( self ):
1017 if self.progress_timer_id is not None:
1018 gobject.source_remove( self.progress_timer_id )
1019 self.progress_timer_id = None
1021 def get_coverart_size( self ):
1022 if platform.MAEMO:
1023 if self.__gui_root.fullscreen:
1024 size = util.coverart_sizes['maemo fullscreen']
1025 else:
1026 size = util.coverart_sizes['maemo']
1027 else:
1028 size = util.coverart_sizes['normal']
1030 return size, size
1032 def set_coverart( self, pixbuf ):
1033 self.cover_art.set_from_pixbuf(pixbuf)
1034 self.cover_art.show()
1035 self.has_coverart = True
1037 def set_metadata( self, tag_message ):
1038 tags = { 'title': self.title_label, 'artist': self.artist_label,
1039 'album': self.album_label }
1041 # set the coverart
1042 if tag_message.has_key('image') and tag_message['image'] is not None:
1043 value = tag_message['image']
1045 pbl = gtk.gdk.PixbufLoader()
1046 try:
1047 pbl.write(value)
1048 pbl.close()
1049 pixbuf = pbl.get_pixbuf()
1050 pixbuf = pixbuf.scale_simple(settings.config.getint("options", "cover_height"),
1051 settings.config.getint("options", "cover_height"), gtk.gdk.INTERP_BILINEAR )
1052 self.set_coverart(pixbuf)
1053 except Exception, e:
1054 self.__log.exception('Error setting coverart...')
1056 # set the text metadata
1057 for tag,value in tag_message.iteritems():
1058 if tags.has_key(tag) and value is not None and value.strip():
1059 try:
1060 tags[tag].set_markup('<big>'+cgi.escape(value)+'</big>')
1061 except TypeError, e:
1062 self.__log.exception(str(e))
1063 tags[tag].set_alignment( 0.5*int(not self.has_coverart), 0.5)
1064 tags[tag].show()
1066 if tag == 'title':
1067 # make the title bold
1068 tags[tag].set_markup('<b><big>'+cgi.escape(value)+'</big></b>')
1070 if not platform.MAEMO:
1071 value += ' - Panucci'
1073 if platform.FREMANTLE and len(value) > 25:
1074 value = value[:24] + '...'
1076 self.__gui_root.main_window.set_title( value )
1078 def do_seek(self, seek_amount):
1079 seek_amount = seek_amount*10**9
1080 resp = None
1081 if not settings.config.getboolean("options", "seek_back") or player.playlist.start_of_playlist() or seek_amount > 0:
1082 resp = player.do_seek(from_current=seek_amount)
1083 else:
1084 pos_int, dur_int = player.get_position_duration()
1085 if pos_int + seek_amount >= 0:
1086 resp = player.do_seek(from_current=seek_amount)
1087 else:
1088 player.playlist.prev()
1089 pos_int, dur_int = player.get_position_duration()
1090 resp = player.do_seek(from_beginning=dur_int+seek_amount)
1091 if resp:
1092 # Preemptively update the progressbar to make seeking smoother
1093 self.set_progress_callback( *resp )
1095 def run(filename=None):
1096 PanucciGUI(filename)
1097 gtk.main()
1099 if __name__ == '__main__':
1100 log.error( 'Use the "panucci" executable to run this program.' )
1101 log.error( 'Exiting...' )
1102 sys.exit(1)