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