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