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
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')
45 pynotify
.init('Panucci')
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 ##################################################
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)
81 self
.app
= hildon
.Program()
82 if platform
.FREMANTLE
:
83 window
= hildon
.StackableWindow()
85 window
= hildon
.Window()
86 self
.app
.add_window(window
)
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())
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
)
118 if platform
.FREMANTLE
:
119 window
.set_app_menu(self
.create_app_menu())
121 window
.set_menu(self
.create_menu())
122 window
.add(self
.__player
_tab
)
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)
131 menu_vbox
.pack_end(self
.__player
_tab
, True, True, 6)
133 # Tie it all together!
134 self
.__ignore
_queue
_check
= False
135 self
.__window
_fullscreen
= False
137 if platform
.MAEMO
and interface
.headset_device
:
138 # Enable play/pause with headset button
139 interface
.headset_device
.connect_to_signal('Condition', \
140 self
.handle_headset_button
)
141 system_bus
= dbus
.SystemBus()
143 # Monitor connection state of BT headset
144 # I haven't seen this option before "settings.play_on_headset"
145 PATH
= '/org/freedesktop/Hal/devices/computer_logicaldev_input_1'
146 def handler_func(device_path
):
147 if device_path
== PATH
and not player
.playing
:
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':
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',
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
)
191 def create_actions(self
):
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
)
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
)
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)
245 self
.action_play_mode_all
.set_active(True)
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
):
301 action
.connect_proxy(b
)
307 def create_app_menu(self
):
308 menu
= hildon
.AppMenu()
310 for action
in (self
.action_settings
,
311 self
.action_playlist
,
313 self
.action_open_dir
,
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(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
)
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
)
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"),
419 gtk
.DIALOG_MODAL | gtk
.DIALOG_DESTROY_WITH_PARENT
)
421 # Maemo 5 has an implicit "close" button (tap outside of dialog)
422 if not platform
.FREMANTLE
:
423 dialog
.add_button(gtk
.STOCK_CLOSE
, gtk
.RESPONSE_ACCEPT
)
425 table
= gtk
.Table(5, 2, True)
426 dialog
.vbox
.add(table
)
427 b
= gtk
.CheckButton()
428 self
.action_lock_progress
.connect_proxy(b
)
429 table
.attach(b
, 0, 1, 0, 1)
430 b
= gtk
.CheckButton()
431 self
.action_dual_action_button
.connect_proxy(b
)
432 table
.attach(b
, 0, 1, 1, 2)
433 b
= gtk
.CheckButton()
434 self
.action_stay_at_end
.connect_proxy(b
)
435 table
.attach(b
, 0, 1, 2, 3)
436 b
= gtk
.CheckButton()
437 self
.action_seek_back
.connect_proxy(b
)
438 table
.attach(b
, 0, 1, 3, 4)
439 b
= gtk
.CheckButton()
440 self
.action_scrolling_labels
.connect_proxy(b
)
441 table
.attach(b
, 0, 1, 4, 5)
442 label
= gtk
.Label(_("Play Mode"))
443 table
.attach(label
, 1, 2, 0, 1)
444 ra
= gtk
.RadioButton()
445 table
.attach(ra
, 1, 2, 1, 2)
446 rb
= gtk
.RadioButton()
448 table
.attach(rb
, 1, 2, 2, 3)
449 rc
= gtk
.RadioButton()
451 table
.attach(rc
, 1, 2, 3, 4)
452 rd
= gtk
.RadioButton()
454 table
.attach(rd
, 1, 2, 4, 5)
455 if settings
.config
.get("options", "play_mode") == "single":
457 elif settings
.config
.get("options", "play_mode") == "random":
459 elif settings
.config
.get("options", "play_mode") == "repeat":
463 self
.action_play_mode_all
.connect_proxy(ra
)
464 self
.action_play_mode_single
.connect_proxy(rb
)
465 self
.action_play_mode_random
.connect_proxy(rc
)
466 self
.action_play_mode_repeat
.connect_proxy(rd
)
468 response
= dialog
.run()
471 def notify(self
, message
):
472 """ Sends a notification using pynotify, returns message """
473 if platform
.DESKTOP
and have_pynotify
:
474 icon
= util
.find_data_file('panucci_64x64.png')
475 notification
= pynotify
.Notification(self
.main_window
.get_title(), message
, icon
)
477 elif platform
.FREMANTLE
:
478 hildon
.hildon_banner_show_information(self
.main_window
, \
481 # Note: This won't work if we're not in the gtk main loop
482 markup
= '<b>%s</b>\n<small>%s</small>' % (self
.main_window
.get_title(), message
)
483 hildon
.hildon_banner_show_information_with_markup(self
.main_window
, None, markup
)
485 def destroy(self
, widget
):
486 self
.main_window
.hide()
490 def set_progress_indicator(self
, loading_title
=False):
491 if platform
.FREMANTLE
:
493 self
.main_window
.set_title(_('Loading...'))
494 hildon
.hildon_gtk_window_set_progress_indicator(self
.main_window
, \
496 while gtk
.events_pending():
497 gtk
.main_iteration(False)
499 def show_main_window(self
):
500 self
.main_window
.present()
502 def check_queue(self
):
503 """ Makes sure the queue is saved if it has been modified
504 True means a new file can be opened
505 False means the user does not want to continue """
507 if not self
.__ignore
_queue
_check
and player
.playlist
.queue_modified
:
508 response
= gtkutil
.dialog(
509 self
.main_window
, _('Save current playlist'),
510 _('Current playlist has been modified'),
511 _('Opening a new file will replace the current playlist. ') +
512 _('Do you want to save it before creating a new one?'),
513 affirmative_button
=gtk
.STOCK_SAVE
,
514 negative_button
=_('Discard changes'))
516 self
.__log
.debug('Response to "Save Queue?": %s', response
)
521 return self
.save_to_playlist_callback()
529 def open_file_callback(self
, widget
=None):
530 # set __ingnore__queue_check because we already did the check
531 self
.__ignore
_queue
_check
= True
532 filename
= gtkutil
.get_file_from_filechooser(self
)
533 if filename
is not None:
534 self
._play
_file
(filename
)
536 self
.__ignore
_queue
_check
= False
538 def open_dir_callback(self
, widget
=None):
539 filename
= gtkutil
.get_file_from_filechooser(self
, folder
=True)
540 if filename
is not None:
541 self
._play
_file
(filename
)
543 def save_to_playlist_callback(self
, widget
=None):
544 filename
= gtkutil
.get_file_from_filechooser(
545 self
, save_file
=True, save_to
='playlist.m3u' )
550 if os
.path
.isfile(filename
):
551 response
= gtkutil
.dialog( self
.main_window
, _('File already exists'),
552 _('File already exists'),
553 _('The file %s already exists. You can choose another name or '
554 'overwrite the existing file.') % os
.path
.basename(filename
),
555 affirmative_button
=gtk
.STOCK_SAVE
,
556 negative_button
=_('Rename file'))
564 return self
.save_to_playlist_callback()
566 ext
= util
.detect_filetype(filename
)
567 if not player
.playlist
.save_to_new_playlist(filename
, ext
):
568 self
.notify(_('Error saving playlist...'))
573 def empty_playlist_callback(self
, w
):
574 player
.playlist
.reset_playlist()
575 self
.__playlist
_tab
.treeview
.get_model().clear()
577 def delete_all_bookmarks_callback(self
, widget
=None):
578 response
= gtkutil
.dialog(
579 self
.main_window
, _('Delete All Bookmarks'),
580 _('Would you like to delete all bookmarks?'),
581 _('By accepting all bookmarks in the database will be deleted.'),
582 negative_button
=None)
584 self
.__log
.debug('Response to "Delete all bookmarks?": %s', response
)
587 player
.playlist
.delete_all_bookmarks()
588 model
= self
.__playlist
_tab
.treeview
.get_model()
590 for row
in iter(model
):
591 while model
.iter_has_child(row
.iter):
592 bkmk_iter
= model
.iter_children(row
.iter)
593 model
.remove(bkmk_iter
)
595 def set_boolean_config_callback(self
, w
):
597 settings
.config
.set("options", w
.get_name(), "true")
599 settings
.config
.set("options", w
.get_name(), "false")
601 def scrolling_labels_callback(self
, w
):
602 self
.set_boolean_config_callback(w
)
603 self
.__player
_tab
.title_label
.scrolling
= w
.get_active()
605 def set_play_mode_callback(self
, w
):
606 settings
.config
.set("options", "play_mode", w
.get_name())
608 def __get_fullscreen(self
):
609 return self
.__window
_fullscreen
611 def __set_fullscreen(self
, value
):
612 if value
!= self
.__window
_fullscreen
:
614 self
.main_window
.fullscreen()
616 self
.main_window
.unfullscreen()
618 self
.__window
_fullscreen
= value
619 player
.playlist
.send_metadata()
621 fullscreen
= property( __get_fullscreen
, __set_fullscreen
)
623 def on_key_press(self
, widget
, event
):
625 if event
.keyval
== gtk
.keysyms
.F6
:
626 self
.fullscreen
= not self
.fullscreen
628 def on_recent_file_activate(self
, widget
, filepath
):
629 self
._play
_file
(filepath
)
631 def on_file_queued(self
, filepath
, success
, notify
):
633 filename
= os
.path
.basename(filepath
)
636 self
.notify( '%s added successfully.' % filename
))
639 self
.notify( 'Error adding %s to the queue.' % filename
))
641 def about_callback(self
, widget
):
642 if platform
.FREMANTLE
:
643 from panucci
.gtkui
.gtkaboutdialog
import HeAboutDialog
644 HeAboutDialog
.present(self
.main_window
, panucci
.__version
__)
646 from panucci
.gtkui
.gtkaboutdialog
import AboutDialog
647 AboutDialog(self
.main_window
, panucci
.__version
__)
649 def _play_file(self
, filename
, pause_on_load
=False):
650 player
.playlist
.load( os
.path
.abspath(filename
) )
652 if player
.playlist
.is_empty
:
655 def handle_headset_button(self
, event
, button
):
656 if event
== 'ButtonPressed' and button
== 'phone':
657 player
.play_pause_toggle()
659 def __select_current_item( self
):
660 # Select the currently playing track in the playlist tab
661 # and switch to it (so we can edit bookmarks, etc.. there)
662 self
.__playlist
_tab
.select_current_item()
663 self
.playlist_window
.show()
665 ##################################################
667 ##################################################
668 class PlayerTab(ObservableService
, gtk
.HBox
):
669 """ The tab that holds the player elements """
671 signals
= [ 'select-current-item-request', ]
673 def __init__(self
, gui_root
):
674 self
.__log
= logging
.getLogger('panucci.panucci.PlayerTab')
675 self
.__gui
_root
= gui_root
677 gtk
.HBox
.__init
__(self
)
678 ObservableService
.__init
__(self
, self
.signals
, self
.__log
)
681 self
.progress_timer_id
= None
683 self
.recent_files
= []
684 self
.make_player_tab()
685 self
.has_coverart
= False
687 #settings.register( 'enable_dual_action_btn_changed',
688 # self.on_dual_action_setting_changed )
689 #settings.register( 'dual_action_button_delay_changed',
690 # self.on_dual_action_setting_changed )
691 #settings.register( 'scrolling_labels_changed', lambda v:
692 # setattr( self.title_label, 'scrolling', v ) )
694 player
.register( 'stopped', self
.on_player_stopped
)
695 player
.register( 'playing', self
.on_player_playing
)
696 player
.register( 'paused', self
.on_player_paused
)
697 player
.register( 'eof', self
.on_player_eof
)
698 player
.playlist
.register( 'end-of-playlist',
699 self
.on_player_end_of_playlist
)
700 player
.playlist
.register( 'new-track-loaded',
701 self
.on_player_new_track
)
702 player
.playlist
.register( 'new-metadata-available',
703 self
.on_player_new_metadata
)
704 player
.playlist
.register( 'reset-playlist',
705 self
.on_player_reset_playlist
)
707 def make_player_tab(self
):
708 main_vbox
= gtk
.VBox()
709 main_vbox
.set_spacing(6)
711 self
.pack_start(main_vbox
, True, True)
713 # a hbox to hold the cover art and metadata vbox
714 metadata_hbox
= gtk
.HBox()
715 metadata_hbox
.set_spacing(6)
716 main_vbox
.pack_start(metadata_hbox
, True, False)
718 self
.cover_art
= gtk
.Image()
719 metadata_hbox
.pack_start( self
.cover_art
, False, False )
721 # vbox to hold metadata
722 metadata_vbox
= gtk
.VBox()
723 metadata_vbox
.pack_start(gtk
.Image(), True, True)
724 self
.artist_label
= gtk
.Label('')
725 self
.artist_label
.set_ellipsize(pango
.ELLIPSIZE_END
)
726 metadata_vbox
.pack_start(self
.artist_label
, False, False)
727 self
.album_label
= gtk
.Label('')
728 if platform
.FREMANTLE
:
729 hildon
.hildon_helper_set_logical_font(self
.album_label
, 'SmallSystemFont')
730 hildon
.hildon_helper_set_logical_color(self
.album_label
, gtk
.RC_FG
, gtk
.STATE_NORMAL
, 'SecondaryTextColor')
732 self
.album_label
.modify_font(pango
.FontDescription('normal 8'))
733 self
.album_label
.set_ellipsize(pango
.ELLIPSIZE_END
)
734 metadata_vbox
.pack_start(self
.album_label
, False, False)
735 self
.title_label
= widgets
.ScrollingLabel('',
736 settings
.config
.get("options", "scrolling_color"),
739 delay_btwn_scrolls
=5000,
741 self
.title_label
.scrolling
= settings
.config
.getboolean("options", "scrolling_labels")
742 metadata_vbox
.pack_start(self
.title_label
, False, False)
743 metadata_vbox
.pack_start(gtk
.Image(), True, True)
744 metadata_hbox
.pack_start( metadata_vbox
, True, True )
746 progress_eventbox
= gtk
.EventBox()
747 progress_eventbox
.set_events(gtk
.gdk
.BUTTON_PRESS_MASK
)
748 progress_eventbox
.connect(
749 'button-press-event', self
.on_progressbar_changed
)
750 self
.progress
= gtk
.ProgressBar()
751 # make the progress bar more "finger-friendly"
752 if platform
.FREMANTLE
:
753 self
.progress
.set_size_request(-1, 100)
755 self
.progress
.set_size_request(-1, 50)
756 progress_eventbox
.add(self
.progress
)
757 main_vbox
.pack_start( progress_eventbox
, False, False )
759 # make the button box
760 buttonbox
= gtk
.HBox()
762 # A wrapper to help create DualActionButtons with the right settings
763 def create_da(widget
, action
, widget2
=None, action2
=None):
764 if platform
.FREMANTLE
:
768 return widgets
.DualActionButton(widget
, action
, settings
.config
, widget2
, action2
)
770 self
.rrewind_button
= create_da(
771 gtkutil
.generate_image('media-skip-backward.png'),
772 lambda: self
.do_seek(-1*settings
.config
.getint('options', 'seek_long')),
773 gtkutil
.generate_image(gtk
.STOCK_GOTO_FIRST
, True),
774 player
.playlist
.prev
)
775 buttonbox
.add(self
.rrewind_button
)
777 self
.rewind_button
= create_da(
778 gtkutil
.generate_image('media-seek-backward.png'),
779 lambda: self
.do_seek(-1*settings
.config
.getint('options', 'seek_short')))
780 buttonbox
.add(self
.rewind_button
)
782 self
.play_pause_button
= gtk
.Button('')
783 gtkutil
.image(self
.play_pause_button
, 'media-playback-start.png')
784 self
.play_pause_button
.connect( 'clicked',
785 self
.on_btn_play_pause_clicked
)
786 self
.play_pause_button
.set_sensitive(False)
787 buttonbox
.add(self
.play_pause_button
)
789 self
.forward_button
= create_da(
790 gtkutil
.generate_image('media-seek-forward.png'),
791 lambda: self
.do_seek(settings
.config
.getint('options', 'seek_short')))
792 buttonbox
.add(self
.forward_button
)
794 self
.fforward_button
= create_da(
795 gtkutil
.generate_image('media-skip-forward.png'),
796 lambda: self
.do_seek(settings
.config
.getint('options', 'seek_long')),
797 gtkutil
.generate_image(gtk
.STOCK_GOTO_LAST
, True),
798 player
.playlist
.next
)
799 buttonbox
.add(self
.fforward_button
)
801 self
.bookmarks_button
= create_da(
802 gtkutil
.generate_image('bookmark-new.png'),
803 player
.add_bookmark_at_current_position
,
804 gtkutil
.generate_image(gtk
.STOCK_JUMP_TO
, True),
805 lambda *args
: self
.notify('select-current-item-request'))
806 buttonbox
.add(self
.bookmarks_button
)
807 self
.set_controls_sensitivity(False)
809 if platform
.FREMANTLE
:
810 for child
in buttonbox
.get_children():
811 if isinstance(child
, gtk
.Button
):
812 child
.set_name('HildonButton-thumb')
813 buttonbox
.set_size_request(-1, 105)
815 main_vbox
.pack_start(buttonbox
, False, False)
818 self
.__gui
_root
.main_window
.connect( 'key-press-event',
821 # Disable focus for all widgets, so we can use the cursor
822 # keys + enter to directly control our media player, which
823 # is handled by "key-press-event"
825 self
.rrewind_button
, self
.rewind_button
,
826 self
.play_pause_button
, self
.forward_button
,
827 self
.fforward_button
, self
.progress
,
828 self
.bookmarks_button
, ):
829 w
.unset_flags(gtk
.CAN_FOCUS
)
831 def set_controls_sensitivity(self
, sensitive
):
832 for button
in self
.forward_button
, self
.rewind_button
, \
833 self
.fforward_button
, self
.rrewind_button
:
835 button
.set_sensitive(sensitive
)
837 # the play/pause button should always be available except
838 # for when the player starts without a file
839 self
.play_pause_button
.set_sensitive(True)
841 def on_dual_action_setting_changed( self
, *args
):
842 for button
in self
.forward_button
, self
.rewind_button
, \
843 self
.fforward_button
, self
.rrewind_button
, \
844 self
.bookmarks_button
:
846 button
.set_longpress_enabled( settings
.config
.getboolean("options", "enable_dual_action_btn") )
847 button
.set_duration( settings
.config
.getfloat("options", "dual_action_button_delay") )
849 def on_key_press(self
, widget
, event
):
851 if event
.keyval
== gtk
.keysyms
.Left
: # seek back
852 self
.do_seek( -1 * settings
.seek_long
)
853 elif event
.keyval
== gtk
.keysyms
.Right
: # seek forward
854 self
.do_seek( settings
.seek_long
)
855 elif event
.keyval
== gtk
.keysyms
.Return
: # play/pause
856 self
.on_btn_play_pause_clicked()
858 def on_player_stopped(self
):
859 self
.stop_progress_timer()
860 self
.set_controls_sensitivity(False)
861 gtkutil
.image(self
.play_pause_button
, 'media-playback-start.png')
863 def on_player_playing(self
):
864 self
.start_progress_timer()
865 gtkutil
.image(self
.play_pause_button
, 'media-playback-pause.png')
866 self
.set_controls_sensitivity(True)
867 if platform
.FREMANTLE
:
868 hildon
.hildon_gtk_window_set_progress_indicator(\
869 self
.__gui
_root
.main_window
, False)
871 def on_player_eof(self
):
872 play_mode
= settings
.config
.get("options", "play_mode")
873 if play_mode
== "single":
874 if not settings
.config
.getboolean("options", "stay_at_end"):
875 self
.on_player_end_of_playlist(False)
876 elif play_mode
== "random":
877 player
.playlist
.random()
878 elif play_mode
== "repeat":
879 player
.playlist
.next(True)
881 if player
.playlist
.end_of_playlist():
882 if not settings
.config
.getboolean("options", "stay_at_end"):
883 player
.playlist
.next(False)
885 player
.playlist
.next(False)
887 def on_player_new_track(self
):
888 for widget
in [self
.title_label
,self
.artist_label
,self
.album_label
]:
889 widget
.set_markup('')
892 self
.cover_art
.hide()
893 self
.has_coverart
= False
895 def on_player_new_metadata(self
):
896 self
.metadata
= player
.playlist
.get_file_metadata()
897 self
.set_metadata(self
.metadata
)
899 if not player
.playing
:
900 position
= player
.playlist
.get_current_position()
901 estimated_length
= self
.metadata
.get('length', 0)
902 self
.set_progress_callback( position
, estimated_length
)
903 player
.set_position_duration(position
, 0)
905 def on_player_paused( self
, position
, duration
):
906 self
.stop_progress_timer() # This should save some power
907 self
.set_progress_callback( position
, duration
)
908 gtkutil
.image(self
.play_pause_button
, 'media-playback-start.png')
910 def on_player_end_of_playlist(self
, loop
):
912 player
.stop_end_of_playlist()
913 estimated_length
= self
.metadata
.get('length', 0)
914 self
.set_progress_callback( 0, estimated_length
)
915 player
.set_position_duration(0, 0)
917 def on_player_reset_playlist(self
):
918 self
.on_player_stopped()
919 self
.on_player_new_track()
920 self
.reset_progress()
922 def reset_progress(self
):
923 self
.progress
.set_fraction(0)
924 self
.set_progress_callback(0,0)
926 def set_progress_callback(self
, time_elapsed
, total_time
):
927 """ times must be in nanoseconds """
928 time_string
= "%s / %s" % ( util
.convert_ns(time_elapsed
),
929 util
.convert_ns(total_time
) )
930 self
.progress
.set_text( time_string
)
931 fraction
= float(time_elapsed
) / float(total_time
) if total_time
else 0
932 self
.progress
.set_fraction( fraction
)
934 def on_progressbar_changed(self
, widget
, event
):
935 if ( not settings
.config
.getboolean("options", "lock_progress") and
936 event
.type == gtk
.gdk
.BUTTON_PRESS
and event
.button
== 1 ):
937 new_fraction
= event
.x
/float(widget
.get_allocation().width
)
938 resp
= player
.do_seek(percent
=new_fraction
)
940 # Preemptively update the progressbar to make seeking smoother
941 self
.set_progress_callback( *resp
)
943 def on_btn_play_pause_clicked(self
, widget
=None):
944 player
.play_pause_toggle()
946 def progress_timer_callback( self
):
947 if player
.playing
and not player
.seeking
:
948 pos_int
, dur_int
= player
.get_position_duration()
949 # This prevents bogus values from being set while seeking
950 if ( pos_int
> 10**9 ) and ( dur_int
> 10**9 ):
951 self
.set_progress_callback( pos_int
, dur_int
)
954 def start_progress_timer( self
):
955 if self
.progress_timer_id
is not None:
956 self
.stop_progress_timer()
958 self
.progress_timer_id
= gobject
.timeout_add(
959 1000, self
.progress_timer_callback
)
961 def stop_progress_timer( self
):
962 if self
.progress_timer_id
is not None:
963 gobject
.source_remove( self
.progress_timer_id
)
964 self
.progress_timer_id
= None
966 def get_coverart_size( self
):
968 if self
.__gui
_root
.fullscreen
:
969 size
= util
.coverart_sizes
['maemo fullscreen']
971 size
= util
.coverart_sizes
['maemo']
973 size
= util
.coverart_sizes
['normal']
977 def set_coverart( self
, pixbuf
):
978 self
.cover_art
.set_from_pixbuf(pixbuf
)
979 self
.cover_art
.show()
980 self
.has_coverart
= True
982 def set_metadata( self
, tag_message
):
983 tags
= { 'title': self
.title_label
, 'artist': self
.artist_label
,
984 'album': self
.album_label
}
987 if tag_message
.has_key('image') and tag_message
['image'] is not None:
988 value
= tag_message
['image']
990 pbl
= gtk
.gdk
.PixbufLoader()
995 x
, y
= self
.get_coverart_size()
996 pixbuf
= pbl
.get_pixbuf()
997 pixbuf
= pixbuf
.scale_simple( x
, y
, gtk
.gdk
.INTERP_BILINEAR
)
998 self
.set_coverart(pixbuf
)
1000 self
.__log
.exception('Error setting coverart...')
1002 # set the text metadata
1003 for tag
,value
in tag_message
.iteritems():
1004 if tags
.has_key(tag
) and value
is not None and value
.strip():
1006 tags
[tag
].set_markup('<big>'+cgi
.escape(value
)+'</big>')
1007 except TypeError, e
:
1008 self
.__log
.exception(str(e
))
1009 tags
[tag
].set_alignment( 0.5*int(not self
.has_coverart
), 0.5)
1013 # make the title bold
1014 tags
[tag
].set_markup('<b><big>'+cgi
.escape(value
)+'</big></b>')
1016 if not platform
.MAEMO
:
1017 value
+= ' - Panucci'
1019 if platform
.FREMANTLE
and len(value
) > 25:
1020 value
= value
[:24] + '...'
1022 self
.__gui
_root
.main_window
.set_title( value
)
1024 def do_seek(self
, seek_amount
):
1025 seek_amount
= seek_amount
*10**9
1027 if not settings
.config
.getboolean("options", "seek_back") or player
.playlist
.start_of_playlist() or seek_amount
> 0:
1028 resp
= player
.do_seek(from_current
=seek_amount
)
1030 pos_int
, dur_int
= player
.get_position_duration()
1031 if pos_int
+ seek_amount
>= 0:
1032 resp
= player
.do_seek(from_current
=seek_amount
)
1034 player
.playlist
.prev()
1035 pos_int
, dur_int
= player
.get_position_duration()
1036 resp
= player
.do_seek(from_beginning
=dur_int
+seek_amount
)
1038 # Preemptively update the progressbar to make seeking smoother
1039 self
.set_progress_callback( *resp
)
1041 def run(filename
=None):
1042 PanucciGUI(filename
)
1045 if __name__
== '__main__':
1046 log
.error( 'Use the "panucci" executable to run this program.' )
1047 log
.error( 'Exiting...' )