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
.player
.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
)
204 self
.action_lock_progress
= gtk
.ToggleAction('lock_progress', 'Lock Progress Bar', None, None)
205 self
.action_lock_progress
.connect("activate", self
.set_boolean_config_callback
)
206 self
.action_lock_progress
.set_active(self
.config
.getboolean("options", "lock_progress"))
207 self
.action_dual_action_button
= gtk
.ToggleAction('dual_action_button', 'Dual Action Button', None, None)
208 self
.action_dual_action_button
.connect("activate", self
.set_boolean_config_callback
)
209 self
.action_dual_action_button
.set_active(self
.config
.getboolean("options", "dual_action_button"))
210 self
.action_stay_at_end
= gtk
.ToggleAction('stay_at_end', 'Stay at End', None, None)
211 self
.action_stay_at_end
.connect("activate", self
.set_boolean_config_callback
)
212 self
.action_stay_at_end
.set_active(self
.config
.getboolean("options", "stay_at_end"))
213 self
.action_seek_back
= gtk
.ToggleAction('seek_back', 'Seek Back', None, None)
214 self
.action_seek_back
.connect("activate", self
.set_boolean_config_callback
)
215 self
.action_seek_back
.set_active(self
.config
.getboolean("options", "seek_back"))
216 self
.action_scrolling_labels
= gtk
.ToggleAction('scrolling_labels', 'Scrolling Labels', None, None)
217 self
.action_scrolling_labels
.connect("activate", self
.scrolling_labels_callback
)
218 self
.action_scrolling_labels
.set_active(self
.config
.getboolean("options", "scrolling_labels"))
219 self
.action_play_mode
= gtk
.Action('play_mode', 'Play Mode', None, None)
220 self
.action_play_mode_all
= gtk
.RadioAction('all', 'All', None, None, 0)
221 self
.action_play_mode_all
.connect("activate", self
.set_play_mode_callback
)
222 self
.action_play_mode_single
= gtk
.RadioAction('single', 'Single', None, None, 1)
223 self
.action_play_mode_single
.connect("activate", self
.set_play_mode_callback
)
224 self
.action_play_mode_single
.set_group(self
.action_play_mode_all
)
225 self
.action_play_mode_random
= gtk
.RadioAction('random', 'Random', None, None, 1)
226 self
.action_play_mode_random
.connect("activate", self
.set_play_mode_callback
)
227 self
.action_play_mode_random
.set_group(self
.action_play_mode_all
)
228 self
.action_play_mode_repeat
= gtk
.RadioAction('repeat', 'Repeat', None, None, 1)
229 self
.action_play_mode_repeat
.connect("activate", self
.set_play_mode_callback
)
230 self
.action_play_mode_repeat
.set_group(self
.action_play_mode_all
)
231 if self
.config
.get("options", "play_mode") == "single":
232 self
.action_play_mode_single
.set_active(True)
233 elif self
.config
.get("options", "play_mode") == "random":
234 self
.action_play_mode_random
.set_active(True)
235 elif self
.config
.get("options", "play_mode") == "repeat":
236 self
.action_play_mode_repeat
.set_active(True)
238 self
.action_play_mode_all
.set_active(True)
240 self
.action_about
= gtk
.Action('about', _('About'), _('Show application version'), gtk
.STOCK_ABOUT
)
241 self
.action_about
.connect('activate', self
.about_callback
)
243 def create_desktop_menu(self
, menu_bar
):
244 file_menu_item
= gtk
.MenuItem(_('File'))
245 file_menu
= gtk
.Menu()
246 file_menu
.append(self
.action_open
.create_menu_item())
247 file_menu
.append(self
.action_open_dir
.create_menu_item())
248 file_menu
.append(self
.action_save
.create_menu_item())
249 file_menu
.append(self
.action_empty_playlist
.create_menu_item())
250 file_menu
.append(self
.action_delete_bookmarks
.create_menu_item())
251 file_menu
.append(gtk
.SeparatorMenuItem())
252 file_menu
.append(self
.action_quit
.create_menu_item())
253 file_menu_item
.set_submenu(file_menu
)
254 menu_bar
.append(file_menu_item
)
256 tools_menu_item
= gtk
.MenuItem(_('Tools'))
257 tools_menu
= gtk
.Menu()
258 tools_menu
.append(self
.action_playlist
.create_menu_item())
259 #tools_menu.append(self.action_settings.create_menu_item())
260 tools_menu_item
.set_submenu(tools_menu
)
261 menu_bar
.append(tools_menu_item
)
263 settings_menu_item
= gtk
.MenuItem(_('Settings'))
264 settings_menu
= gtk
.Menu()
265 settings_menu
.append(self
.action_lock_progress
.create_menu_item())
266 settings_menu
.append(self
.action_dual_action_button
.create_menu_item())
267 settings_menu
.append(self
.action_stay_at_end
.create_menu_item())
268 settings_menu
.append(self
.action_seek_back
.create_menu_item())
269 settings_menu
.append(self
.action_scrolling_labels
.create_menu_item())
270 play_mode_menu_item
= self
.action_play_mode
.create_menu_item()
271 settings_menu
.append(play_mode_menu_item
)
272 play_mode_menu
= gtk
.Menu()
273 play_mode_menu_item
.set_submenu(play_mode_menu
)
274 play_mode_menu
.append(self
.action_play_mode_all
.create_menu_item())
275 play_mode_menu
.append(self
.action_play_mode_single
.create_menu_item())
276 play_mode_menu
.append(self
.action_play_mode_random
.create_menu_item())
277 play_mode_menu
.append(self
.action_play_mode_repeat
.create_menu_item())
278 settings_menu_item
.set_submenu(settings_menu
)
279 menu_bar
.append(settings_menu_item
)
281 help_menu_item
= gtk
.MenuItem(_('Help'))
282 help_menu
= gtk
.Menu()
283 help_menu
.append(self
.action_about
.create_menu_item())
284 help_menu_item
.set_submenu(help_menu
)
285 menu_bar
.append(help_menu_item
)
287 def create_playlist_app_menu(self
):
288 menu
= hildon
.AppMenu()
290 for action
in (self
.action_save
,
291 self
.action_delete_bookmarks
):
293 action
.connect_proxy(b
)
299 def create_app_menu(self
):
300 menu
= hildon
.AppMenu()
302 for action
in (self
.action_settings
,
303 self
.action_playlist
,
305 self
.action_open_dir
,
306 self
.action_empty_playlist
,
309 action
.connect_proxy(b
)
315 def create_menu(self
):
319 menu_open
= gtk
.ImageMenuItem(_('Add File'))
321 gtk
.image_new_from_stock(gtk
.STOCK_NEW
, gtk
.ICON_SIZE_MENU
))
322 menu_open
.connect("activate", self
.open_file_callback
)
323 menu
.append(menu_open
)
325 menu_open
= gtk
.ImageMenuItem(_('Add Folder'))
327 gtk
.image_new_from_stock(gtk
.STOCK_OPEN
, gtk
.ICON_SIZE_MENU
))
328 menu_open
.connect("activate", self
.open_dir_callback
)
329 menu
.append(menu_open
)
331 # the recent files menu
332 self
.menu_recent
= gtk
.MenuItem(_('Open recent playlist'))
333 menu
.append(self
.menu_recent
)
334 self
.create_recent_files_menu()
336 menu
.append(gtk
.SeparatorMenuItem())
338 menu_save
= gtk
.ImageMenuItem(_('Save current playlist'))
340 gtk
.image_new_from_stock(gtk
.STOCK_SAVE_AS
, gtk
.ICON_SIZE_MENU
))
341 menu_save
.connect("activate", self
.save_to_playlist_callback
)
342 menu
.append(menu_save
)
344 menu_save
= gtk
.ImageMenuItem(_('Delete Playlist'))
346 gtk
.image_new_from_stock(gtk
.STOCK_DELETE
, gtk
.ICON_SIZE_MENU
))
347 menu_save
.connect("activate", self
.empty_playlist_callback
)
348 menu
.append(menu_save
)
350 menu_save
= gtk
.ImageMenuItem(_('Delete All Bookmarks'))
352 gtk
.image_new_from_stock(gtk
.STOCK_DELETE
, gtk
.ICON_SIZE_MENU
))
353 menu_save
.connect("activate", self
.delete_all_bookmarks_callback
)
354 menu
.append(menu_save
)
356 menu
.append(gtk
.SeparatorMenuItem())
358 # the settings sub-menu
359 menu_settings
= gtk
.MenuItem(_('Settings'))
360 menu
.append(menu_settings
)
362 menu_settings_sub
= gtk
.Menu()
363 menu_settings
.set_submenu(menu_settings_sub
)
365 menu_settings_enable_dual_action
= gtk
.CheckMenuItem(_('Enable dual-action buttons') )
366 menu_settings_enable_dual_action
.connect('toggled', self
.set_dual_action_button_callback
)
367 menu_settings_enable_dual_action
.set_active(self
.config
.getboolean("options", "dual_action_button"))
368 menu_settings_sub
.append(menu_settings_enable_dual_action
)
370 menu_settings_lock_progress
= gtk
.CheckMenuItem(_('Lock Progress Bar'))
371 menu_settings_lock_progress
.connect('toggled', self
.lock_progress_callback
)
372 menu_settings_lock_progress
.set_active(self
.config
.getboolean("options", "lock_progress"))
373 menu_settings_sub
.append(menu_settings_lock_progress
)
375 menu_about
= gtk
.ImageMenuItem(gtk
.STOCK_ABOUT
)
376 menu_about
.connect("activate", self
.about_callback
)
377 menu
.append(menu_about
)
379 menu
.append(gtk
.SeparatorMenuItem())
381 menu_quit
= gtk
.ImageMenuItem(gtk
.STOCK_QUIT
)
382 menu_quit
.connect("activate", self
.destroy
)
383 menu
.append(menu_quit
)
387 def create_recent_files_menu( self
):
388 max_files
= self
.config
.getint("options", "max_recent_files")
389 self
.recent_files
= player
.playlist
.get_recent_files(max_files
)
390 menu_recent_sub
= gtk
.Menu()
392 if len(self
.recent_files
) > 0:
393 for f
in self
.recent_files
:
394 # don't include the temporary playlist in the file list
395 if f
== panucci
.PLAYLIST_FILE
: continue
396 # don't include non-existant files
397 if not os
.path
.exists( f
): continue
398 filename
, extension
= os
.path
.splitext(os
.path
.basename(f
))
399 menu_item
= gtk
.MenuItem( filename
.replace('_', ' '))
400 menu_item
.connect('activate', self
.on_recent_file_activate
, f
)
401 menu_recent_sub
.append(menu_item
)
403 menu_item
= gtk
.MenuItem(_('No recent files available.'))
404 menu_item
.set_sensitive(False)
405 menu_recent_sub
.append(menu_item
)
407 self
.menu_recent
.set_submenu(menu_recent_sub
)
409 def notify(self
, message
):
410 """ Sends a notification using pynotify, returns message """
411 if platform
.DESKTOP
and have_pynotify
:
412 icon
= util
.find_data_file('panucci_64x64.png')
413 notification
= pynotify
.Notification(self
.main_window
.get_title(), message
, icon
)
415 elif platform
.FREMANTLE
:
416 hildon
.hildon_banner_show_information(self
.main_window
, \
419 # Note: This won't work if we're not in the gtk main loop
420 markup
= '<b>%s</b>\n<small>%s</small>' % (self
.main_window
.get_title(), message
)
421 hildon
.hildon_banner_show_information_with_markup(self
.main_window
, None, markup
)
423 def destroy(self
, widget
):
424 self
.main_window
.hide()
425 self
.playlist
.player
.quit()
426 util
.write_config(self
.config
)
429 def set_progress_indicator(self
, loading_title
=False):
430 if platform
.FREMANTLE
:
432 self
.main_window
.set_title(_('Loading...'))
433 hildon
.hildon_gtk_window_set_progress_indicator(self
.main_window
, \
435 while gtk
.events_pending():
436 gtk
.main_iteration(False)
438 def show_main_window(self
):
439 self
.main_window
.present()
441 def check_queue(self
):
442 """ Makes sure the queue is saved if it has been modified
443 True means a new file can be opened
444 False means the user does not want to continue """
446 if not self
.__ignore
_queue
_check
and self
.playlist
.queue_modified
:
447 response
= gtkutil
.dialog(
448 self
.main_window
, _('Save current playlist'),
449 _('Current playlist has been modified'),
450 _('Opening a new file will replace the current playlist. ') +
451 _('Do you want to save it before creating a new one?'),
452 affirmative_button
=gtk
.STOCK_SAVE
,
453 negative_button
=_('Discard changes'))
455 self
.__log
.debug('Response to "Save Queue?": %s', response
)
460 return self
.save_to_playlist_callback()
468 def open_file_callback(self
, widget
=None):
469 # set __ingnore__queue_check because we already did the check
470 self
.__ignore
_queue
_check
= True
471 filename
= gtkutil
.get_file_from_filechooser(self
)
472 if filename
is not None:
473 self
._play
_file
(filename
)
475 self
.__ignore
_queue
_check
= False
477 def open_dir_callback(self
, widget
=None):
478 filename
= gtkutil
.get_file_from_filechooser(self
, folder
=True)
479 if filename
is not None:
480 self
._play
_file
(filename
)
482 def save_to_playlist_callback(self
, widget
=None):
483 filename
= gtkutil
.get_file_from_filechooser(
484 self
, save_file
=True, save_to
='playlist.m3u' )
489 if os
.path
.isfile(filename
):
490 response
= gtkutil
.dialog( self
.main_window
, _('File already exists'),
491 _('File already exists'),
492 _('The file %s already exists. You can choose another name or '
493 'overwrite the existing file.') % os
.path
.basename(filename
),
494 affirmative_button
=gtk
.STOCK_SAVE
,
495 negative_button
=_('Rename file'))
503 return self
.save_to_playlist_callback()
505 ext
= util
.detect_filetype(filename
)
506 if not self
.playlist
.save_to_new_playlist(filename
, ext
):
507 self
.notify(_('Error saving playlist...'))
512 def empty_playlist_callback(self
, w
):
513 self
.playlist
.reset_playlist()
514 self
.__playlist
_tab
.treeview
.get_model().clear()
516 def delete_all_bookmarks_callback(self
, widget
=None):
517 response
= gtkutil
.dialog(
518 self
.main_window
, _('Delete All Bookmarks'),
519 _('Would you like to delete all bookmarks?'),
520 _('By accepting all bookmarks in the database will be deleted.'),
521 negative_button
=None)
523 self
.__log
.debug('Response to "Delete all bookmarks?": %s', response
)
526 self
.playlist
.delete_all_bookmarks()
527 model
= self
.__playlist
_tab
.treeview
.get_model()
529 for row
in iter(model
):
530 while model
.iter_has_child(row
.iter):
531 bkmk_iter
= model
.iter_children(row
.iter)
532 model
.remove(bkmk_iter
)
534 def set_boolean_config_callback(self
, w
):
536 self
.config
.set("options", w
.get_name(), "true")
538 self
.config
.set("options", w
.get_name(), "false")
540 def scrolling_labels_callback(self
, w
):
541 self
.set_boolean_config_callback(w
)
542 self
.__player
_tab
.title_label
.scrolling
= w
.get_active()
544 def set_play_mode_callback(self
, w
):
545 self
.config
.set("options", "play_mode", w
.get_name())
547 def __get_fullscreen(self
):
548 return self
.__window
_fullscreen
550 def __set_fullscreen(self
, value
):
551 if value
!= self
.__window
_fullscreen
:
553 self
.main_window
.fullscreen()
555 self
.main_window
.unfullscreen()
557 self
.__window
_fullscreen
= value
558 self
.playlist
.send_metadata()
560 fullscreen
= property( __get_fullscreen
, __set_fullscreen
)
562 def on_key_press(self
, widget
, event
):
564 if event
.keyval
== gtk
.keysyms
.F6
:
565 self
.fullscreen
= not self
.fullscreen
567 def on_recent_file_activate(self
, widget
, filepath
):
568 self
._play
_file
(filepath
)
570 def on_file_queued(self
, filepath
, success
, notify
):
572 filename
= os
.path
.basename(filepath
)
575 self
.notify( '%s added successfully.' % filename
))
578 self
.notify( 'Error adding %s to the queue.' % filename
))
580 def settings_callback(self
, widget
):
581 from panucci
.gtkui
.gtksettingsdialog
import SettingsDialog
584 def about_callback(self
, widget
):
585 if platform
.FREMANTLE
:
586 from panucci
.gtkui
.gtkaboutdialog
import HeAboutDialog
587 HeAboutDialog
.present(self
.main_window
, panucci
.__version
__)
589 from panucci
.gtkui
.gtkaboutdialog
import AboutDialog
590 AboutDialog(self
.main_window
, panucci
.__version
__)
592 def _play_file(self
, filename
, pause_on_load
=False):
593 self
.playlist
.load( os
.path
.abspath(filename
) )
595 if self
.playlist
.is_empty
:
598 def handle_headset_button(self
, event
, button
):
599 if event
== 'ButtonPressed' and button
== 'phone':
600 self
.playlist
.player
.play_pause_toggle()
602 def __select_current_item( self
):
603 # Select the currently playing track in the playlist tab
604 # and switch to it (so we can edit bookmarks, etc.. there)
605 self
.__playlist
_tab
.select_current_item()
606 self
.playlist_window
.show()
608 ##################################################
610 ##################################################
611 class PlayerTab(ObservableService
, gtk
.HBox
):
612 """ The tab that holds the player elements """
614 signals
= [ 'select-current-item-request', ]
616 def __init__(self
, gui_root
):
617 self
.__log
= logging
.getLogger('panucci.panucci.PlayerTab')
618 self
.__gui
_root
= gui_root
619 self
.config
= gui_root
.config
620 self
.playlist
= gui_root
.playlist
622 gtk
.HBox
.__init
__(self
)
623 ObservableService
.__init
__(self
, self
.signals
, self
.__log
)
626 self
.progress_timer_id
= None
628 self
.recent_files
= []
629 self
.make_player_tab()
630 self
.has_coverart
= False
632 #settings.register( 'enable_dual_action_btn_changed',
633 # self.on_dual_action_setting_changed )
634 #settings.register( 'dual_action_button_delay_changed',
635 # self.on_dual_action_setting_changed )
636 #settings.register( 'scrolling_labels_changed', lambda v:
637 # setattr( self.title_label, 'scrolling', v ) )
639 self
.playlist
.player
.register( 'stopped', self
.on_player_stopped
)
640 self
.playlist
.player
.register( 'playing', self
.on_player_playing
)
641 self
.playlist
.player
.register( 'paused', self
.on_player_paused
)
642 self
.playlist
.player
.register( 'eof', self
.on_player_eof
)
643 self
.playlist
.register( 'end-of-playlist',
644 self
.on_player_end_of_playlist
)
645 self
.playlist
.register( 'new-track-loaded',
646 self
.on_player_new_track
)
647 self
.playlist
.register( 'new-metadata-available',
648 self
.on_player_new_metadata
)
649 self
.playlist
.register( 'reset-playlist',
650 self
.on_player_reset_playlist
)
652 def make_player_tab(self
):
653 main_vbox
= gtk
.VBox()
654 main_vbox
.set_spacing(6)
656 self
.pack_start(main_vbox
, True, True)
658 # a hbox to hold the cover art and metadata vbox
659 metadata_hbox
= gtk
.HBox()
660 metadata_hbox
.set_spacing(6)
661 main_vbox
.pack_start(metadata_hbox
, True, False)
663 self
.cover_art
= gtk
.Image()
664 metadata_hbox
.pack_start( self
.cover_art
, False, False )
666 # vbox to hold metadata
667 metadata_vbox
= gtk
.VBox()
668 metadata_vbox
.pack_start(gtk
.Image(), True, True)
669 self
.artist_label
= gtk
.Label('')
670 self
.artist_label
.set_ellipsize(pango
.ELLIPSIZE_END
)
671 metadata_vbox
.pack_start(self
.artist_label
, False, False)
672 separator
= gtk
.Label("")
673 separator
.set_size_request(-1, 10)
674 metadata_vbox
.pack_start(separator
, False, False)
675 self
.album_label
= gtk
.Label('')
676 if platform
.FREMANTLE
:
677 hildon
.hildon_helper_set_logical_font(self
.album_label
, 'SmallSystemFont')
678 hildon
.hildon_helper_set_logical_color(self
.album_label
, gtk
.RC_FG
, gtk
.STATE_NORMAL
, 'SecondaryTextColor')
680 self
.album_label
.modify_font(pango
.FontDescription('normal 8'))
681 self
.album_label
.set_ellipsize(pango
.ELLIPSIZE_END
)
682 metadata_vbox
.pack_start(self
.album_label
, False, False)
683 separator
= gtk
.Label("")
684 separator
.set_size_request(-1, 10)
685 metadata_vbox
.pack_start(separator
, False, False)
686 self
.title_label
= widgets
.ScrollingLabel('',
687 self
.config
.get("options", "scrolling_color"),
690 delay_btwn_scrolls
=5000,
692 self
.title_label
.scrolling
= self
.config
.getboolean("options", "scrolling_labels")
693 metadata_vbox
.pack_start(self
.title_label
, False, False)
694 metadata_vbox
.pack_start(gtk
.Image(), True, True)
695 metadata_hbox
.pack_start( metadata_vbox
, True, True )
697 progress_eventbox
= gtk
.EventBox()
698 progress_eventbox
.set_events(gtk
.gdk
.BUTTON_PRESS_MASK
)
699 progress_eventbox
.connect(
700 'button-press-event', self
.on_progressbar_changed
)
701 self
.progress
= gtk
.ProgressBar()
702 self
.progress
.set_size_request(-1, self
.config
.getint("options", "progress_height"))
703 progress_eventbox
.add(self
.progress
)
704 main_vbox
.pack_start( progress_eventbox
, False, False )
706 # make the button box
707 buttonbox
= gtk
.HBox()
709 # A wrapper to help create DualActionButtons with the right settings
710 def create_da(widget
, action
, widget2
=None, action2
=None):
711 if platform
.FREMANTLE
:
715 return widgets
.DualActionButton(widget
, action
, self
.config
, widget2
, action2
)
717 self
.rrewind_button
= create_da(
718 gtkutil
.generate_image('media-skip-backward.png'),
719 lambda: self
.do_seek(-1*self
.config
.getint('options', 'seek_long')),
720 gtkutil
.generate_image(gtk
.STOCK_GOTO_FIRST
, True),
722 buttonbox
.add(self
.rrewind_button
)
724 self
.rewind_button
= create_da(
725 gtkutil
.generate_image('media-seek-backward.png'),
726 lambda: self
.do_seek(-1*self
.config
.getint('options', 'seek_short')))
727 buttonbox
.add(self
.rewind_button
)
729 self
.play_pause_button
= gtk
.Button('')
730 gtkutil
.image(self
.play_pause_button
, 'media-playback-start.png')
731 self
.play_pause_button
.connect( 'clicked',
732 self
.on_btn_play_pause_clicked
)
733 self
.play_pause_button
.set_sensitive(False)
734 buttonbox
.add(self
.play_pause_button
)
736 self
.forward_button
= create_da(
737 gtkutil
.generate_image('media-seek-forward.png'),
738 lambda: self
.do_seek(self
.config
.getint('options', 'seek_short')))
739 buttonbox
.add(self
.forward_button
)
741 self
.fforward_button
= create_da(
742 gtkutil
.generate_image('media-skip-forward.png'),
743 lambda: self
.do_seek(self
.config
.getint('options', 'seek_long')),
744 gtkutil
.generate_image(gtk
.STOCK_GOTO_LAST
, True),
746 buttonbox
.add(self
.fforward_button
)
748 self
.bookmarks_button
= create_da(
749 gtkutil
.generate_image('bookmark-new.png'),
750 self
.playlist
.player
.add_bookmark_at_current_position
,
751 gtkutil
.generate_image(gtk
.STOCK_JUMP_TO
, True),
752 lambda *args
: self
.notify('select-current-item-request'))
753 buttonbox
.add(self
.bookmarks_button
)
754 self
.set_controls_sensitivity(False)
756 if platform
.FREMANTLE
:
757 for child
in buttonbox
.get_children():
758 if isinstance(child
, gtk
.Button
):
759 child
.set_name('HildonButton-thumb')
761 buttonbox
.set_size_request(-1, self
.config
.getint("options", "button_height"))
762 main_vbox
.pack_start(buttonbox
, False, False)
765 self
.__gui
_root
.main_window
.connect( 'key-press-event',
768 # Disable focus for all widgets, so we can use the cursor
769 # keys + enter to directly control our media player, which
770 # is handled by "key-press-event"
772 self
.rrewind_button
, self
.rewind_button
,
773 self
.play_pause_button
, self
.forward_button
,
774 self
.fforward_button
, self
.progress
,
775 self
.bookmarks_button
, ):
776 w
.unset_flags(gtk
.CAN_FOCUS
)
778 def set_controls_sensitivity(self
, sensitive
):
779 for button
in self
.forward_button
, self
.rewind_button
, \
780 self
.fforward_button
, self
.rrewind_button
:
782 button
.set_sensitive(sensitive
)
784 # the play/pause button should always be available except
785 # for when the player starts without a file
786 self
.play_pause_button
.set_sensitive(True)
788 def on_dual_action_setting_changed( self
, *args
):
789 for button
in self
.forward_button
, self
.rewind_button
, \
790 self
.fforward_button
, self
.rrewind_button
, \
791 self
.bookmarks_button
:
793 button
.set_longpress_enabled( self
.config
.getboolean("options", "dual_action_button") )
794 button
.set_duration( self
.config
.getfloat("options", "dual_action_button_delay") )
796 def on_key_press(self
, widget
, event
):
798 if event
.keyval
== gtk
.keysyms
.Left
: # seek back
799 self
.do_seek( -1 * self
.config
.getint('options', 'seek_long') )
800 elif event
.keyval
== gtk
.keysyms
.Right
: # seek forward
801 self
.do_seek( self
.config
.getint('options', 'seek_long') )
802 elif event
.keyval
== gtk
.keysyms
.Return
: # play/pause
803 self
.on_btn_play_pause_clicked()
805 def on_player_stopped(self
):
806 self
.stop_progress_timer()
807 self
.set_controls_sensitivity(False)
808 gtkutil
.image(self
.play_pause_button
, 'media-playback-start.png')
810 def on_player_playing(self
):
811 self
.start_progress_timer()
812 gtkutil
.image(self
.play_pause_button
, 'media-playback-pause.png')
813 self
.set_controls_sensitivity(True)
814 if platform
.FREMANTLE
:
815 hildon
.hildon_gtk_window_set_progress_indicator(\
816 self
.__gui
_root
.main_window
, False)
818 def on_player_eof(self
):
819 play_mode
= self
.config
.get("options", "play_mode")
820 if play_mode
== "single":
821 if not self
.config
.getboolean("options", "stay_at_end"):
822 self
.on_player_end_of_playlist(False)
823 elif play_mode
== "random":
824 self
.playlist
.random()
825 elif play_mode
== "repeat":
826 self
.playlist
.next(True)
828 if self
.playlist
.end_of_playlist():
829 if not self
.config
.getboolean("options", "stay_at_end"):
830 self
.playlist
.next(False)
832 self
.playlist
.next(False)
834 def on_player_new_track(self
):
835 for widget
in [self
.title_label
,self
.artist_label
,self
.album_label
]:
836 widget
.set_markup('')
839 self
.cover_art
.hide()
840 self
.has_coverart
= False
842 def on_player_new_metadata(self
):
843 self
.metadata
= self
.playlist
.get_file_metadata()
844 self
.set_metadata(self
.metadata
)
846 if not self
.playlist
.player
.playing
:
847 position
= self
.playlist
.get_current_position()
848 estimated_length
= self
.metadata
.get('length', 0)
849 self
.set_progress_callback( position
, estimated_length
)
850 self
.playlist
.player
.set_position_duration(position
, 0)
852 def on_player_paused( self
, position
, duration
):
853 self
.stop_progress_timer() # This should save some power
854 self
.set_progress_callback( position
, duration
)
855 gtkutil
.image(self
.play_pause_button
, 'media-playback-start.png')
857 def on_player_end_of_playlist(self
, loop
):
859 self
.playlist
.player
.stop_end_of_playlist()
860 estimated_length
= self
.metadata
.get('length', 0)
861 self
.set_progress_callback( 0, estimated_length
)
862 self
.playlist
.player
.set_position_duration(0, 0)
864 def on_player_reset_playlist(self
):
865 self
.on_player_stopped()
866 self
.on_player_new_track()
867 self
.reset_progress()
869 def reset_progress(self
):
870 self
.progress
.set_fraction(0)
871 self
.set_progress_callback(0,0)
872 self
.__gui
_root
.main_window
.set_title("Panucci")
874 def set_progress_callback(self
, time_elapsed
, total_time
):
875 """ times must be in nanoseconds """
876 time_string
= "%s / %s" % ( util
.convert_ns(time_elapsed
),
877 util
.convert_ns(total_time
) )
878 self
.progress
.set_text( time_string
)
879 fraction
= float(time_elapsed
) / float(total_time
) if total_time
else 0
880 self
.progress
.set_fraction( fraction
)
882 def on_progressbar_changed(self
, widget
, event
):
883 if ( not self
.config
.getboolean("options", "lock_progress") and
884 event
.type == gtk
.gdk
.BUTTON_PRESS
and event
.button
== 1 ):
885 new_fraction
= event
.x
/float(widget
.get_allocation().width
)
886 resp
= self
.playlist
.player
.do_seek(percent
=new_fraction
)
888 # Preemptively update the progressbar to make seeking smoother
889 self
.set_progress_callback( *resp
)
891 def on_btn_play_pause_clicked(self
, widget
=None):
892 self
.playlist
.player
.play_pause_toggle()
894 def progress_timer_callback( self
):
895 if self
.playlist
.player
.playing
and not self
.playlist
.player
.seeking
:
896 pos_int
, dur_int
= self
.playlist
.player
.get_position_duration()
897 # This prevents bogus values from being set while seeking
898 if ( pos_int
> 10**9 ) and ( dur_int
> 10**9 ):
899 self
.set_progress_callback( pos_int
, dur_int
)
902 def start_progress_timer( self
):
903 if self
.progress_timer_id
is not None:
904 self
.stop_progress_timer()
906 self
.progress_timer_id
= gobject
.timeout_add(
907 1000, self
.progress_timer_callback
)
909 def stop_progress_timer( self
):
910 if self
.progress_timer_id
is not None:
911 gobject
.source_remove( self
.progress_timer_id
)
912 self
.progress_timer_id
= None
914 def get_coverart_size( self
):
916 if self
.__gui
_root
.fullscreen
:
917 size
= util
.coverart_sizes
['maemo fullscreen']
919 size
= util
.coverart_sizes
['maemo']
921 size
= util
.coverart_sizes
['normal']
925 def set_coverart( self
, pixbuf
):
926 self
.cover_art
.set_from_pixbuf(pixbuf
)
927 self
.cover_art
.show()
928 self
.has_coverart
= True
930 def set_metadata( self
, tag_message
):
931 tags
= { 'title': self
.title_label
, 'artist': self
.artist_label
,
932 'album': self
.album_label
}
935 if tag_message
.has_key('image') and tag_message
['image'] is not None:
936 value
= tag_message
['image']
938 pbl
= gtk
.gdk
.PixbufLoader()
942 pixbuf
= pbl
.get_pixbuf()
943 pixbuf
= pixbuf
.scale_simple(self
.config
.getint("options", "cover_height"),
944 self
.config
.getint("options", "cover_height"), gtk
.gdk
.INTERP_BILINEAR
)
945 self
.set_coverart(pixbuf
)
947 self
.__log
.exception('Error setting coverart...')
949 # set the text metadata
950 for tag
,value
in tag_message
.iteritems():
951 if tags
.has_key(tag
) and value
is not None and value
.strip():
953 _str
= '<big>' + cgi
.escape(value
) + '</big>'
955 _str
= cgi
.escape(value
)
957 _str
= '<b><big>' + cgi
.escape(value
) + '</big></b>'
958 if not platform
.MAEMO
:
959 value
+= ' - Panucci'
960 if platform
.FREMANTLE
and len(value
) > 25:
961 value
= value
[:24] + '...'
962 self
.__gui
_root
.main_window
.set_title( value
)
965 tags
[tag
].set_markup(_str
)
967 self
.__log
.exception(str(e
))
968 tags
[tag
].set_alignment( 0.5*int(not self
.has_coverart
), 0.5)
971 def do_seek(self
, seek_amount
):
972 seek_amount
= seek_amount
*10**9
974 if not self
.config
.getboolean("options", "seek_back") or self
.playlist
.start_of_playlist() or seek_amount
> 0:
975 resp
= self
.playlist
.player
.do_seek(from_current
=seek_amount
)
977 pos_int
, dur_int
= self
.playlist
.player
.get_position_duration()
978 if pos_int
+ seek_amount
>= 0:
979 resp
= self
.playlist
.player
.do_seek(from_current
=seek_amount
)
982 pos_int
, dur_int
= self
.playlist
.player
.get_position_duration()
983 resp
= self
.playlist
.player
.do_seek(from_beginning
=dur_int
+seek_amount
)
985 # Preemptively update the progressbar to make seeking smoother
986 self
.set_progress_callback( *resp
)