3 # This file is part of Panucci.
4 # Copyright (c) 2008-2010 The Panucci Audiobook and Podcast Player 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 # Based on http://thpinfo.com/2008/panucci/:
20 # A resuming media player for Podcasts and Audiobooks
21 # Copyright (c) 2008-05-26 Thomas Perl <thpinfo.com>
22 # (based on http://pygstdocs.berlios.de/pygst-tutorial/seeking.html)
25 from __future__
import absolute_import
39 from panucci
import widgets
40 from panucci
import util
42 log
= logging
.getLogger('panucci.panucci')
48 if util
.platform
.MAEMO
:
49 log
.critical( 'Using GTK widgets, install "python2.5-hildon" '
50 'for this to work properly.' )
52 from panucci
.simplegconf
import gconf
53 from panucci
.settings
import settings
54 from panucci
.player
import player
55 from panucci
.dbusinterface
import interface
56 from panucci
.services
import ObservableService
58 about_name
= 'Panucci'
59 about_text
= _('Resuming audiobook and podcast player')
60 about_authors
= ['Thomas Perl', 'Nick (nikosapi)', 'Matthew Taylor']
61 about_website
= 'http://panucci.garage.maemo.org/'
66 'maemo fullscreen' : 275,
69 gtk
.about_dialog_set_url_hook(util
.open_link
, None)
70 gtk
.icon_size_register('panucci-button', 32, 32)
72 def generate_image(filename
, is_stock
=False):
75 image
= gtk
.image_new_from_stock(
76 filename
, gtk
.icon_size_from_name('panucci-button') )
78 filename
= util
.find_image(filename
)
79 if filename
is not None:
80 image
= gtk
.image_new_from_file(filename
)
82 if util
.platform
.MAEMO
:
83 image
.set_padding(20, 20)
85 image
.set_padding(5, 5)
89 def image(widget
, filename
, is_stock
=False):
90 child
= widget
.get_child()
93 image
= generate_image(filename
, is_stock
)
97 def dialog( toplevel_window
, title
, question
, description
,
98 affirmative_button
=gtk
.STOCK_YES
, negative_button
=gtk
.STOCK_NO
,
99 abortion_button
=gtk
.STOCK_CANCEL
):
101 """Present the user with a yes/no/cancel dialog.
102 The return value is either True, False or None, depending on which
103 button has been pressed in the dialog:
105 affirmative button (default: Yes) => True
106 negative button (defaut: No) => False
107 abortion button (default: Cancel) => None
109 When the dialog is closed with the "X" button in the window manager
110 decoration, the return value is always None (same as abortion button).
112 You can set any of the affirmative_button, negative_button or
113 abortion_button values to "None" to hide the corresponding action.
115 dlg
= gtk
.MessageDialog( toplevel_window
, gtk
.DIALOG_MODAL
,
116 gtk
.MESSAGE_QUESTION
, message_format
=question
)
120 if abortion_button
is not None:
121 dlg
.add_button(abortion_button
, gtk
.RESPONSE_CANCEL
)
122 if negative_button
is not None:
123 dlg
.add_button(negative_button
, gtk
.RESPONSE_NO
)
124 if affirmative_button
is not None:
125 dlg
.add_button(affirmative_button
, gtk
.RESPONSE_YES
)
127 dlg
.format_secondary_text(description
)
132 if response
== gtk
.RESPONSE_YES
:
134 elif response
== gtk
.RESPONSE_NO
:
136 elif response
in [gtk
.RESPONSE_CANCEL
, gtk
.RESPONSE_DELETE_EVENT
]:
139 def get_file_from_filechooser(
140 toplevel_window
, folder
=False, save_file
=False, save_to
=None):
143 open_action
= gtk
.FILE_CHOOSER_ACTION_SELECT_FOLDER
145 open_action
= gtk
.FILE_CHOOSER_ACTION_OPEN
147 if util
.platform
.MAEMO
:
149 args
= ( toplevel_window
, gtk
.FILE_CHOOSER_ACTION_SAVE
)
151 args
= ( toplevel_window
, open_action
)
153 dlg
= hildon
.FileChooserDialog( *args
)
156 args
= ( _('Select file to save playlist to'), None,
157 gtk
.FILE_CHOOSER_ACTION_SAVE
,
158 (( gtk
.STOCK_CANCEL
, gtk
.RESPONSE_REJECT
,
159 gtk
.STOCK_SAVE
, gtk
.RESPONSE_OK
)) )
161 args
= ( _('Select podcast or audiobook'), None, open_action
,
162 (( gtk
.STOCK_CANCEL
, gtk
.RESPONSE_REJECT
,
163 gtk
.STOCK_OPEN
, gtk
.RESPONSE_OK
)) )
165 dlg
= gtk
.FileChooserDialog(*args
)
167 current_folder
= os
.path
.expanduser(settings
.last_folder
)
169 if current_folder
is not None and os
.path
.isdir(current_folder
):
170 dlg
.set_current_folder(current_folder
)
172 if save_file
and save_to
is not None:
173 dlg
.set_current_name(save_to
)
175 if dlg
.run() == gtk
.RESPONSE_OK
:
176 filename
= dlg
.get_filename()
177 settings
.last_folder
= dlg
.get_current_folder()
184 def set_stock_button_text( button
, text
):
185 alignment
= button
.get_child()
186 hbox
= alignment
.get_child()
187 image
, label
= hbox
.get_children()
190 ##################################################
192 ##################################################
193 class PanucciGUI(object):
194 """ The object that holds the entire panucci gui """
196 def __init__(self
, filename
=None):
197 self
.__log
= logging
.getLogger('panucci.panucci.PanucciGUI')
198 interface
.register_gui(self
)
200 # Build the base ui (window and menubar)
201 if util
.platform
.MAEMO
:
202 self
.app
= hildon
.Program()
203 window
= hildon
.Window()
204 self
.app
.add_window(window
)
206 window
= gtk
.Window(gtk
.WINDOW_TOPLEVEL
)
208 self
.main_window
= window
209 window
.set_title('Panucci')
210 self
.window_icon
= util
.find_image('panucci.png')
211 if self
.window_icon
is not None:
212 window
.set_icon_from_file( self
.window_icon
)
213 window
.set_default_size(400, -1)
214 window
.set_border_width(0)
215 window
.connect("destroy", self
.destroy
)
217 # Add the tabs (they are private to prevent us from trying to do
218 # something like gui_root.player_tab.some_function() from inside
219 # playlist_tab or vice-versa)
220 self
.__player
_tab
= PlayerTab(self
)
221 self
.__playlist
_tab
= PlaylistTab(self
)
223 self
.playlist_window
= gtk
.Window(gtk
.WINDOW_TOPLEVEL
)
224 self
.playlist_window
.connect('delete-event', gtk
.Widget
.hide_on_delete
)
225 self
.playlist_window
.set_title(_('Panucci playlist'))
226 self
.playlist_window
.set_transient_for(self
.main_window
)
227 self
.playlist_window
.add(self
.__playlist
_tab
)
229 self
.create_actions()
231 if util
.platform
.MAEMO
:
232 window
.set_menu(self
.create_menu())
233 window
.add(self
.__player
_tab
)
235 menu_vbox
= gtk
.VBox()
236 menu_vbox
.set_spacing(0)
237 window
.add(menu_vbox
)
238 menu_bar
= gtk
.MenuBar()
239 self
.create_desktop_menu(menu_bar
)
240 menu_vbox
.pack_start(menu_bar
, False, False, 0)
242 menu_vbox
.pack_end(self
.__player
_tab
, True, True, 6)
244 # Tie it all together!
245 self
.__ignore
_queue
_check
= False
246 self
.__window
_fullscreen
= False
248 if util
.platform
.MAEMO
and interface
.headset_device
is not None:
249 # Enable play/pause with headset button
250 interface
.headset_device
.connect_to_signal(
251 'Condition', self
.handle_headset_button
)
253 self
.main_window
.connect('key-press-event', self
.on_key_press
)
254 player
.playlist
.register( 'file_queued', self
.on_file_queued
)
256 self
.__anti
_blank
_timer
= None
257 settings
.register('allow_blanking_changed',self
.__set
_anti
_blank
_timer
)
258 self
.__set
_anti
_blank
_timer
( settings
.allow_blanking
)
260 player
.playlist
.register( 'playlist-to-be-overwritten',
262 self
.__player
_tab
.register( 'select-current-item-request',
263 self
.__select
_current
_item
)
265 self
.main_window
.show_all()
267 # this should be done when the gui is ready
268 player
.init(filepath
=filename
)
270 def create_actions(self
):
271 self
.action_open
= gtk
.Action('open', _('Open'), _('Open a file or playlist'), gtk
.STOCK_OPEN
)
272 self
.action_open
.connect('activate', self
.open_file_callback
)
273 self
.action_save
= gtk
.Action('save', _('Save playlist'), _('Save current playlist to file'), gtk
.STOCK_SAVE_AS
)
274 self
.action_save
.connect('activate', self
.save_to_playlist_callback
)
275 self
.action_playlist
= gtk
.Action('playlist', _('Playlist'), _('Open the current playlist'), None)
276 self
.action_playlist
.connect('activate', lambda a
: self
.playlist_window
.show())
277 self
.action_about
= gtk
.Action('about', _('About Panucci'), _('Show application version'), gtk
.STOCK_ABOUT
)
278 self
.action_about
.connect('activate', self
.about_callback
)
279 self
.action_quit
= gtk
.Action('quit', _('Quit'), _('Close Panucci'), gtk
.STOCK_QUIT
)
280 self
.action_quit
.connect('activate', self
.destroy
)
282 def create_desktop_menu(self
, menu_bar
):
283 file_menu_item
= gtk
.MenuItem(_('File'))
284 file_menu
= gtk
.Menu()
285 file_menu
.append(self
.action_open
.create_menu_item())
286 file_menu
.append(self
.action_save
.create_menu_item())
287 file_menu
.append(gtk
.SeparatorMenuItem())
288 file_menu
.append(self
.action_quit
.create_menu_item())
289 file_menu_item
.set_submenu(file_menu
)
290 menu_bar
.append(file_menu_item
)
292 tools_menu_item
= gtk
.MenuItem(_('Tools'))
293 tools_menu
= gtk
.Menu()
294 tools_menu
.append(self
.action_playlist
.create_menu_item())
295 tools_menu_item
.set_submenu(tools_menu
)
296 menu_bar
.append(tools_menu_item
)
298 help_menu_item
= gtk
.MenuItem(_('Help'))
299 help_menu
= gtk
.Menu()
300 help_menu
.append(self
.action_about
.create_menu_item())
301 help_menu_item
.set_submenu(help_menu
)
302 menu_bar
.append(help_menu_item
)
304 def create_menu(self
):
308 menu_open
= gtk
.ImageMenuItem(_('Open playlist'))
310 gtk
.image_new_from_stock(gtk
.STOCK_OPEN
, gtk
.ICON_SIZE_MENU
))
311 menu_open
.connect("activate", self
.open_file_callback
)
312 menu
.append(menu_open
)
314 # the recent files menu
315 self
.menu_recent
= gtk
.MenuItem(_('Open recent playlist'))
316 menu
.append(self
.menu_recent
)
317 self
.create_recent_files_menu()
319 menu
.append(gtk
.SeparatorMenuItem())
321 menu_save
= gtk
.ImageMenuItem(_('Save current playlist'))
323 gtk
.image_new_from_stock(gtk
.STOCK_SAVE_AS
, gtk
.ICON_SIZE_MENU
))
324 menu_save
.connect("activate", self
.save_to_playlist_callback
)
325 menu
.append(menu_save
)
327 menu
.append(gtk
.SeparatorMenuItem())
329 # the settings sub-menu
330 menu_settings
= gtk
.MenuItem(_('Settings'))
331 menu
.append(menu_settings
)
333 menu_settings_sub
= gtk
.Menu()
334 menu_settings
.set_submenu(menu_settings_sub
)
336 menu_settings_enable_dual_action
= gtk
.CheckMenuItem(
337 _('Enable dual-action buttons') )
338 settings
.attach_checkbutton( menu_settings_enable_dual_action
,
339 'enable_dual_action_btn' )
340 menu_settings_sub
.append(menu_settings_enable_dual_action
)
342 if util
.platform
.MAEMO
:
343 menu_settings_enable_hw_decoding
= gtk
.CheckMenuItem(
344 _('Enable hardware decoding') )
345 settings
.attach_checkbutton( menu_settings_enable_hw_decoding
,
346 'enable_hardware_decoding' )
347 menu_settings_sub
.append(menu_settings_enable_hw_decoding
)
349 menu_settings_lock_progress
= gtk
.CheckMenuItem(_('Lock Progress Bar'))
350 settings
.attach_checkbutton( menu_settings_lock_progress
,
352 menu_settings_sub
.append(menu_settings_lock_progress
)
354 menu_about
= gtk
.ImageMenuItem(gtk
.STOCK_ABOUT
)
355 menu_about
.connect("activate", self
.about_callback
)
356 menu
.append(menu_about
)
358 menu
.append(gtk
.SeparatorMenuItem())
360 menu_quit
= gtk
.ImageMenuItem(gtk
.STOCK_QUIT
)
361 menu_quit
.connect("activate", self
.destroy
)
362 menu
.append(menu_quit
)
366 def create_recent_files_menu( self
):
367 max_files
= settings
.max_recent_files
368 self
.recent_files
= player
.playlist
.get_recent_files(max_files
)
369 menu_recent_sub
= gtk
.Menu()
371 temp_playlist
= os
.path
.expanduser(settings
.temp_playlist
)
373 if len(self
.recent_files
) > 0:
374 for f
in self
.recent_files
:
375 # don't include the temporary playlist in the file list
376 if f
== temp_playlist
: continue
377 # don't include non-existant files
378 if not os
.path
.exists( f
): continue
379 filename
, extension
= os
.path
.splitext(os
.path
.basename(f
))
380 menu_item
= gtk
.MenuItem( filename
.replace('_', ' '))
381 menu_item
.connect('activate', self
.on_recent_file_activate
, f
)
382 menu_recent_sub
.append(menu_item
)
384 menu_item
= gtk
.MenuItem(_('No recent files available.'))
385 menu_item
.set_sensitive(False)
386 menu_recent_sub
.append(menu_item
)
388 self
.menu_recent
.set_submenu(menu_recent_sub
)
390 def destroy(self
, widget
):
394 def show_main_window(self
):
395 self
.main_window
.present()
397 def check_queue(self
):
398 """ Makes sure the queue is saved if it has been modified
399 True means a new file can be opened
400 False means the user does not want to continue """
402 if not self
.__ignore
_queue
_check
and player
.playlist
.queue_modified
:
404 self
.main_window
, _('Save current playlist'),
405 _('Current playlist has been modified'),
406 _('Opening a new file will replace the current playlist. ') +
407 _('Do you want to save it before creating a new one?'),
408 affirmative_button
=gtk
.STOCK_SAVE
,
409 negative_button
=_('Discard changes'))
411 self
.__log
.debug('Response to "Save Queue?": %s', response
)
416 return self
.save_to_playlist_callback()
424 def open_file_callback(self
, widget
=None):
425 if self
.check_queue():
426 # set __ingnore__queue_check because we already did the check
427 self
.__ignore
_queue
_check
= True
428 filename
= get_file_from_filechooser(self
.main_window
)
429 if filename
is not None:
430 self
._play
_file
(filename
)
432 self
.__ignore
_queue
_check
= False
434 def save_to_playlist_callback(self
, widget
=None):
435 filename
= get_file_from_filechooser(
436 self
.main_window
, save_file
=True, save_to
='playlist.m3u' )
441 if os
.path
.isfile(filename
):
442 response
= dialog( self
.main_window
, _('File already exists'),
443 _('File already exists'),
444 _('The file %s already exists. You can choose another name or '
445 'overwrite the existing file.') % os
.path
.basename(filename
),
446 affirmative_button
=gtk
.STOCK_SAVE
,
447 negative_button
=_('Rename file'))
455 return self
.save_to_playlist_callback()
457 ext
= util
.detect_filetype(filename
)
458 if not player
.playlist
.save_to_new_playlist(filename
, ext
):
459 util
.notify(_('Error saving playlist...'))
464 def __get_fullscreen(self
):
465 return self
.__window
_fullscreen
467 def __set_fullscreen(self
, value
):
468 if value
!= self
.__window
_fullscreen
:
470 self
.main_window
.fullscreen()
472 self
.main_window
.unfullscreen()
474 self
.__window
_fullscreen
= value
475 player
.playlist
.send_metadata()
477 fullscreen
= property( __get_fullscreen
, __set_fullscreen
)
479 def on_key_press(self
, widget
, event
):
480 if util
.platform
.MAEMO
:
481 if event
.keyval
== gtk
.keysyms
.F6
:
482 self
.fullscreen
= not self
.fullscreen
484 def on_recent_file_activate(self
, widget
, filepath
):
485 self
._play
_file
(filepath
)
487 def on_file_queued(self
, filepath
, success
, notify
):
489 filename
= os
.path
.basename(filepath
)
492 util
.notify( '%s added successfully.' % filename
))
495 util
.notify( 'Error adding %s to the queue.' % filename
))
497 def about_callback(self
, widget
):
498 dialog
= gtk
.AboutDialog()
499 dialog
.set_website(about_website
)
500 dialog
.set_website_label(about_website
)
501 dialog
.set_name(about_name
)
502 dialog
.set_authors(about_authors
)
503 dialog
.set_comments(about_text
)
504 dialog
.set_version(panucci
.__version
__)
508 def _play_file(self
, filename
, pause_on_load
=False):
509 player
.playlist
.load( os
.path
.abspath(filename
) )
511 if player
.playlist
.is_empty
:
514 def handle_headset_button(self
, event
, button
):
515 if event
== 'ButtonPressed' and button
== 'phone':
516 player
.play_pause_toggle()
518 def __set_anti_blank_timer(self
, allow_blanking
):
519 if util
.platform
.MAEMO
:
520 if allow_blanking
and self
.__anti
_blank
_timer
is not None:
521 self
.__log
.info('Screen blanking enabled.')
522 gobject
.source_remove(self
.__anti
_blank
_timer
)
523 self
.__anti
_blank
_timer
= None
524 elif not allow_blanking
and self
.__anti
_blank
_timer
is None:
525 self
.__log
.info('Attempting to disable screen blanking.')
526 self
.__anti
_blank
_timer
= gobject
.timeout_add(
527 1000 * 59, util
.poke_backlight
)
529 self
.__log
.info('Blanking controls are for Maemo only.')
531 def __select_current_item( self
):
532 # Select the currently playing track in the playlist tab
533 # and switch to it (so we can edit bookmarks, etc.. there)
534 self
.__playlist
_tab
.select_current_item()
535 self
.playlist_window
.show()
537 ##################################################
539 ##################################################
540 class PlayerTab(ObservableService
, gtk
.HBox
):
541 """ The tab that holds the player elements """
543 signals
= [ 'select-current-item-request', ]
545 def __init__(self
, gui_root
):
546 self
.__log
= logging
.getLogger('panucci.panucci.PlayerTab')
547 self
.__gui
_root
= gui_root
549 gtk
.HBox
.__init
__(self
)
550 ObservableService
.__init
__(self
, self
.signals
, self
.__log
)
553 self
.progress_timer_id
= None
554 self
.volume_timer_id
= None
556 self
.recent_files
= []
557 self
.make_player_tab()
558 self
.has_coverart
= False
559 self
.set_volume(settings
.volume
)
561 settings
.register( 'enable_dual_action_btn_changed',
562 self
.on_dual_action_setting_changed
)
563 settings
.register( 'dual_action_button_delay_changed',
564 self
.on_dual_action_setting_changed
)
565 settings
.register( 'volume_changed', self
.set_volume
)
566 settings
.register( 'scrolling_labels_changed', lambda v
:
567 setattr( self
.title_label
, 'scrolling', v
) )
569 player
.register( 'stopped', self
.on_player_stopped
)
570 player
.register( 'playing', self
.on_player_playing
)
571 player
.register( 'paused', self
.on_player_paused
)
572 player
.playlist
.register( 'end-of-playlist',
573 self
.on_player_end_of_playlist
)
574 player
.playlist
.register( 'new-track-loaded',
575 self
.on_player_new_track
)
576 player
.playlist
.register( 'new-metadata-available',
577 self
.on_player_new_metadata
)
579 def make_player_tab(self
):
580 main_vbox
= gtk
.VBox()
581 main_vbox
.set_spacing(6)
583 self
.pack_start(main_vbox
, True, True)
585 # a hbox to hold the cover art and metadata vbox
586 metadata_hbox
= gtk
.HBox()
587 metadata_hbox
.set_spacing(6)
588 main_vbox
.pack_start(metadata_hbox
, True, False)
590 self
.cover_art
= gtk
.Image()
591 metadata_hbox
.pack_start( self
.cover_art
, False, False )
593 # vbox to hold metadata
594 metadata_vbox
= gtk
.VBox()
595 metadata_vbox
.set_spacing(8)
596 empty_label
= gtk
.Label()
597 metadata_vbox
.pack_start(empty_label
, True, True)
598 self
.artist_label
= gtk
.Label('')
599 self
.artist_label
.set_ellipsize(pango
.ELLIPSIZE_END
)
600 metadata_vbox
.pack_start(self
.artist_label
, False, False)
601 self
.album_label
= gtk
.Label('')
602 self
.album_label
.set_ellipsize(pango
.ELLIPSIZE_END
)
603 metadata_vbox
.pack_start(self
.album_label
, False, False)
604 self
.title_label
= widgets
.ScrollingLabel( '',
607 delay_btwn_scrolls
=5000,
609 self
.title_label
.scrolling
= settings
.scrolling_labels
610 metadata_vbox
.pack_start(self
.title_label
, False, False)
611 empty_label
= gtk
.Label()
612 metadata_vbox
.pack_start(empty_label
, True, True)
613 metadata_hbox
.pack_start( metadata_vbox
, True, True )
615 progress_eventbox
= gtk
.EventBox()
616 progress_eventbox
.set_events(gtk
.gdk
.BUTTON_PRESS_MASK
)
617 progress_eventbox
.connect(
618 'button-press-event', self
.on_progressbar_changed
)
619 self
.progress
= gtk
.ProgressBar()
620 # make the progress bar more "finger-friendly"
621 if util
.platform
.MAEMO5
:
622 self
.progress
.set_size_request(-1, 100)
623 elif util
.platform
.MAEMO
:
624 self
.progress
.set_size_request(-1, 50)
625 progress_eventbox
.add(self
.progress
)
626 main_vbox
.pack_start( progress_eventbox
, False, False )
628 # make the button box
629 buttonbox
= gtk
.HBox()
631 # A wrapper to help create DualActionButtons with the right settings
632 create_da
= lambda a
, b
, c
=None, d
=None: widgets
.DualActionButton(
633 a
, b
, c
, d
, settings
.dual_action_button_delay
,
634 settings
.enable_dual_action_btn
)
636 self
.rrewind_button
= create_da(
637 generate_image('media-skip-backward.png'),
638 lambda: self
.do_seek(-1*settings
.seek_long
),
639 generate_image(gtk
.STOCK_GOTO_FIRST
, True),
640 player
.playlist
.prev
)
641 buttonbox
.add(self
.rrewind_button
)
643 self
.rewind_button
= create_da(
644 generate_image('media-seek-backward.png'),
645 lambda: self
.do_seek(-1*settings
.seek_short
))
646 buttonbox
.add(self
.rewind_button
)
648 self
.play_pause_button
= gtk
.Button('')
649 image(self
.play_pause_button
, 'media-playback-start.png')
650 self
.play_pause_button
.connect( 'clicked',
651 self
.on_btn_play_pause_clicked
)
652 self
.play_pause_button
.set_sensitive(False)
653 buttonbox
.add(self
.play_pause_button
)
655 self
.forward_button
= create_da(
656 generate_image('media-seek-forward.png'),
657 lambda: self
.do_seek(settings
.seek_short
))
658 buttonbox
.add(self
.forward_button
)
660 self
.fforward_button
= create_da(
661 generate_image('media-skip-forward.png'),
662 lambda: self
.do_seek(settings
.seek_long
),
663 generate_image(gtk
.STOCK_GOTO_LAST
, True),
664 player
.playlist
.next
)
665 buttonbox
.add(self
.fforward_button
)
667 self
.bookmarks_button
= create_da(
668 generate_image('bookmark-new.png'),
669 player
.add_bookmark_at_current_position
,
670 generate_image(gtk
.STOCK_JUMP_TO
, True),
671 lambda *args
: self
.notify('select-current-item-request'))
672 buttonbox
.add(self
.bookmarks_button
)
673 self
.set_controls_sensitivity(False)
674 main_vbox
.pack_start(buttonbox
, False, False)
676 if util
.platform
.MAEMO
:
677 self
.volume
= hildon
.VVolumebar()
678 self
.volume
.set_property('can-focus', False)
679 self
.volume
.connect('level_changed', self
.volume_changed_hildon
)
680 self
.volume
.connect('mute_toggled', self
.mute_toggled
)
681 self
.__gui
_root
.main_window
.connect( 'key-press-event',
683 if not util
.platform
.MAEMO5
:
684 self
.pack_start(self
.volume
, False, True)
686 # Add a button to pop out the volume bar
687 self
.volume_button
= gtk
.ToggleButton('')
688 image(self
.volume_button
, 'media-speaker.png')
689 self
.volume_button
.connect('clicked', self
.toggle_volumebar
)
691 'show', lambda x
: self
.volume_button
.set_active(True))
693 'hide', lambda x
: self
.volume_button
.set_active(False))
694 if not util
.platform
.MAEMO5
:
695 buttonbox
.add(self
.volume_button
)
696 self
.volume_button
.show()
698 # Disable focus for all widgets, so we can use the cursor
699 # keys + enter to directly control our media player, which
700 # is handled by "key-press-event"
702 self
.rrewind_button
, self
.rewind_button
,
703 self
.play_pause_button
, self
.forward_button
,
704 self
.fforward_button
, self
.progress
,
705 self
.bookmarks_button
, self
.volume_button
, ):
706 w
.unset_flags(gtk
.CAN_FOCUS
)
708 self
.volume
= gtk
.VolumeButton()
709 self
.volume
.connect('value-changed', self
.volume_changed_gtk
)
710 buttonbox
.add(self
.volume
)
713 self
.set_volume(settings
.volume
)
715 def set_controls_sensitivity(self
, sensitive
):
716 for button
in self
.forward_button
, self
.rewind_button
, \
717 self
.fforward_button
, self
.rrewind_button
:
719 button
.set_sensitive(sensitive
)
721 # the play/pause button should always be available except
722 # for when the player starts without a file
723 self
.play_pause_button
.set_sensitive(True)
725 def on_dual_action_setting_changed( self
, *args
):
726 for button
in self
.forward_button
, self
.rewind_button
, \
727 self
.fforward_button
, self
.rrewind_button
, \
728 self
.bookmarks_button
:
730 button
.set_longpress_enabled( settings
.enable_dual_action_btn
)
731 button
.set_duration( settings
.dual_action_button_delay
)
733 def on_key_press(self
, widget
, event
):
734 if util
.platform
.MAEMO
:
735 if event
.keyval
== gtk
.keysyms
.F7
: #plus
736 self
.set_volume( min( 1, self
.get_volume() + 0.10 ))
737 elif event
.keyval
== gtk
.keysyms
.F8
: #minus
738 self
.set_volume( max( 0, self
.get_volume() - 0.10 ))
739 elif event
.keyval
== gtk
.keysyms
.Left
: # seek back
740 self
.do_seek( -1 * settings
.seek_long
)
741 elif event
.keyval
== gtk
.keysyms
.Right
: # seek forward
742 self
.do_seek( settings
.seek_long
)
743 elif event
.keyval
== gtk
.keysyms
.Return
: # play/pause
744 self
.on_btn_play_pause_clicked()
746 # The following two functions get and set the
747 # volume from the volume control widgets.
748 def get_volume(self
):
749 if util
.platform
.MAEMO
:
750 return self
.volume
.get_level()/100.0
752 return self
.volume
.get_value()
754 def set_volume(self
, vol
):
755 """ vol is a float from 0 to 1 """
758 if util
.platform
.MAEMO5
:
759 # No volume setting on Maemo 5
762 if util
.platform
.MAEMO
:
763 self
.volume
.set_level(vol
*100.0)
765 self
.volume
.set_value(vol
)
767 def __set_volume_hide_timer(self
, timeout
, force_show
=False):
768 if force_show
or self
.volume_button
.get_active():
770 if self
.volume_timer_id
is not None:
771 gobject
.source_remove(self
.volume_timer_id
)
772 self
.volume_timer_id
= None
774 self
.volume_timer_id
= gobject
.timeout_add(
775 1000 * timeout
, self
.__volume
_hide
_callback
)
777 def __volume_hide_callback(self
):
778 self
.volume_timer_id
= None
782 def toggle_volumebar(self
, widget
=None):
783 if self
.volume_timer_id
is None:
784 self
.__set
_volume
_hide
_timer
(5)
786 self
.__volume
_hide
_callback
()
788 def volume_changed_gtk(self
, widget
, new_value
=0.5):
789 settings
.volume
= new_value
791 def volume_changed_hildon(self
, widget
):
792 self
.__set
_volume
_hide
_timer
( 4, force_show
=True )
793 settings
.volume
= widget
.get_level()/100.0
795 def mute_toggled(self
, widget
):
796 if widget
.get_mute():
799 settings
.volume
= widget
.get_level()/100.0
801 def on_player_stopped(self
):
802 self
.stop_progress_timer()
803 self
.set_controls_sensitivity(False)
804 image(self
.play_pause_button
, 'media-playback-start.png')
806 def on_player_playing(self
):
807 self
.start_progress_timer()
808 image(self
.play_pause_button
, 'media-playback-pause.png')
809 self
.set_controls_sensitivity(True)
811 def on_player_new_track(self
):
812 for widget
in [self
.title_label
,self
.artist_label
,self
.album_label
]:
813 widget
.set_markup('')
816 self
.cover_art
.hide()
817 self
.has_coverart
= False
819 def on_player_new_metadata(self
):
820 metadata
= player
.playlist
.get_file_metadata()
821 self
.set_metadata(metadata
)
823 if not player
.playing
:
824 position
= player
.playlist
.get_current_position()
825 estimated_length
= metadata
.get('length', 0)
826 self
.set_progress_callback( position
, estimated_length
)
828 def on_player_paused( self
, position
, duration
):
829 self
.stop_progress_timer() # This should save some power
830 self
.set_progress_callback( position
, duration
)
831 image(self
.play_pause_button
, 'media-playback-start.png')
833 def on_player_end_of_playlist(self
, loop
):
836 def reset_progress(self
):
837 self
.progress
.set_fraction(0)
838 self
.set_progress_callback(0,0)
840 def set_progress_callback(self
, time_elapsed
, total_time
):
841 """ times must be in nanoseconds """
842 time_string
= "%s / %s" % ( util
.convert_ns(time_elapsed
),
843 util
.convert_ns(total_time
) )
844 self
.progress
.set_text( time_string
)
845 fraction
= float(time_elapsed
) / float(total_time
) if total_time
else 0
846 self
.progress
.set_fraction( fraction
)
848 def on_progressbar_changed(self
, widget
, event
):
849 if ( not settings
.progress_locked
and
850 event
.type == gtk
.gdk
.BUTTON_PRESS
and event
.button
== 1 ):
851 new_fraction
= event
.x
/float(widget
.get_allocation().width
)
852 resp
= player
.do_seek(percent
=new_fraction
)
854 # Preemptively update the progressbar to make seeking smoother
855 self
.set_progress_callback( *resp
)
857 def on_btn_play_pause_clicked(self
, widget
=None):
858 player
.play_pause_toggle()
860 def progress_timer_callback( self
):
861 if player
.playing
and not player
.seeking
:
862 pos_int
, dur_int
= player
.get_position_duration()
863 # This prevents bogus values from being set while seeking
864 if ( pos_int
> 10**9 ) and ( dur_int
> 10**9 ):
865 self
.set_progress_callback( pos_int
, dur_int
)
868 def start_progress_timer( self
):
869 if self
.progress_timer_id
is not None:
870 self
.stop_progress_timer()
872 self
.progress_timer_id
= gobject
.timeout_add(
873 1000, self
.progress_timer_callback
)
875 def stop_progress_timer( self
):
876 if self
.progress_timer_id
is not None:
877 gobject
.source_remove( self
.progress_timer_id
)
878 self
.progress_timer_id
= None
880 def get_coverart_size( self
):
881 if util
.platform
.MAEMO
:
882 if self
.__gui
_root
.fullscreen
:
883 size
= coverart_sizes
['maemo fullscreen']
885 size
= coverart_sizes
['maemo']
887 size
= coverart_sizes
['normal']
891 def set_coverart( self
, pixbuf
):
892 self
.cover_art
.set_from_pixbuf(pixbuf
)
893 self
.cover_art
.show()
894 self
.has_coverart
= True
896 def set_metadata( self
, tag_message
):
897 tags
= { 'title': self
.title_label
, 'artist': self
.artist_label
,
898 'album': self
.album_label
}
901 if tag_message
.has_key('image') and tag_message
['image'] is not None:
902 value
= tag_message
['image']
904 pbl
= gtk
.gdk
.PixbufLoader()
909 x
, y
= self
.get_coverart_size()
910 pixbuf
= pbl
.get_pixbuf()
911 pixbuf
= pixbuf
.scale_simple( x
, y
, gtk
.gdk
.INTERP_BILINEAR
)
912 self
.set_coverart(pixbuf
)
914 self
.__log
.exception('Error setting coverart...')
916 # set the text metadata
917 for tag
,value
in tag_message
.iteritems():
918 if tags
.has_key(tag
) and value
is not None and value
.strip():
919 tags
[tag
].set_markup('<big>'+value
+'</big>')
920 tags
[tag
].set_alignment( 0.5*int(not self
.has_coverart
), 0.5)
924 # make the title bold
925 tags
[tag
].set_markup('<b><big>'+value
+'</big></b>')
927 if not util
.platform
.MAEMO
:
928 value
+= ' - Panucci'
930 self
.__gui
_root
.main_window
.set_title( value
)
932 def do_seek(self
, seek_amount
):
933 resp
= player
.do_seek(from_current
=seek_amount
*10**9)
935 # Preemptively update the progressbar to make seeking smoother
936 self
.set_progress_callback( *resp
)
940 ##################################################
942 ##################################################
943 class PlaylistTab(gtk
.VBox
):
944 def __init__(self
, main_window
):
945 gtk
.VBox
.__init
__(self
)
946 self
.__log
= logging
.getLogger('panucci.panucci.BookmarksWindow')
947 self
.main
= main_window
949 self
.__model
= gtk
.TreeStore(
950 # uid, name, position
951 gobject
.TYPE_STRING
, gobject
.TYPE_STRING
, gobject
.TYPE_STRING
)
954 self
.treeview
= gtk
.TreeView()
955 self
.treeview
.set_model(self
.__model
)
956 self
.treeview
.set_headers_visible(True)
957 tree_selection
= self
.treeview
.get_selection()
958 # This breaks drag and drop, only use single selection for now
959 # tree_selection.set_mode(gtk.SELECTION_MULTIPLE)
960 tree_selection
.connect('changed', self
.tree_selection_changed
)
962 # The tree lines look nasty on maemo
963 if util
.platform
.DESKTOP
:
964 self
.treeview
.set_enable_tree_lines(True)
967 ncol
= gtk
.TreeViewColumn(_('Name'))
968 ncell
= gtk
.CellRendererText()
969 ncell
.set_property('ellipsize', pango
.ELLIPSIZE_END
)
970 ncell
.set_property('editable', True)
971 ncell
.connect('edited', self
.label_edited
)
972 ncol
.set_expand(True)
973 ncol
.pack_start(ncell
)
974 ncol
.add_attribute(ncell
, 'text', 1)
976 tcol
= gtk
.TreeViewColumn(_('Position'))
977 tcell
= gtk
.CellRendererText()
978 tcol
.pack_start(tcell
)
979 tcol
.add_attribute(tcell
, 'text', 2)
981 self
.treeview
.append_column(ncol
)
982 self
.treeview
.append_column(tcol
)
983 self
.treeview
.connect('drag-data-received', self
.drag_data_recieved
)
984 self
.treeview
.connect('drag_data_get', self
.drag_data_get_data
)
987 ( 'playlist_row_data', gtk
.TARGET_SAME_WIDGET
, 0 ) ]
989 self
.treeview
.enable_model_drag_source(
990 gtk
.gdk
.BUTTON1_MASK
, treeview_targets
, gtk
.gdk
.ACTION_COPY
)
992 self
.treeview
.enable_model_drag_dest(
993 treeview_targets
, gtk
.gdk
.ACTION_COPY
)
995 sw
= gtk
.ScrolledWindow()
996 sw
.set_policy(gtk
.POLICY_AUTOMATIC
, gtk
.POLICY_AUTOMATIC
)
997 sw
.set_shadow_type(gtk
.SHADOW_IN
)
998 sw
.add(self
.treeview
)
1001 self
.hbox
= gtk
.HBox()
1003 self
.add_button
= gtk
.Button(gtk
.STOCK_NEW
)
1004 self
.add_button
.set_use_stock(True)
1005 set_stock_button_text( self
.add_button
, _('Add File') )
1006 self
.add_button
.connect('clicked', self
.add_file
)
1007 self
.hbox
.pack_start(self
.add_button
, True, True)
1009 self
.dir_button
= gtk
.Button(gtk
.STOCK_OPEN
)
1010 self
.dir_button
.set_use_stock(True)
1011 set_stock_button_text( self
.dir_button
, _('Add Directory') )
1012 self
.dir_button
.connect('clicked', self
.add_directory
)
1013 self
.hbox
.pack_start(self
.dir_button
, True, True)
1015 self
.remove_button
= widgets
.DualActionButton(
1016 generate_image(gtk
.STOCK_REMOVE
, True),
1017 self
.remove_bookmark
,
1018 generate_image(gtk
.STOCK_CANCEL
, True),
1019 lambda *a
: player
.playlist
.reset_playlist() )
1020 #self.remove_button.set_use_stock(True)
1021 #self.remove_button.connect('clicked', self.remove_bookmark)
1022 self
.hbox
.pack_start(self
.remove_button
, True, True)
1024 self
.jump_button
= gtk
.Button(gtk
.STOCK_JUMP_TO
)
1025 self
.jump_button
.set_use_stock(True)
1026 self
.jump_button
.connect('clicked', self
.jump_bookmark
)
1027 self
.hbox
.pack_start(self
.jump_button
, True, True)
1029 self
.info_button
= gtk
.Button()
1030 self
.info_button
.add(
1031 gtk
.image_new_from_stock(gtk
.STOCK_INFO
, gtk
.ICON_SIZE_BUTTON
))
1032 self
.info_button
.connect('clicked', self
.show_playlist_item_details
)
1033 self
.hbox
.pack_start(self
.info_button
, True, True)
1035 self
.pack_start(self
.hbox
, False, True)
1037 player
.playlist
.register( 'file_queued',
1038 lambda x
,y
,z
: self
.update_model() )
1039 player
.playlist
.register( 'bookmark_added', self
.on_bookmark_added
)
1043 def tree_selection_changed(self
, treeselection
):
1044 count
= treeselection
.count_selected_rows()
1045 self
.remove_button
.set_sensitive(count
> 0)
1046 self
.jump_button
.set_sensitive(count
== 1)
1047 self
.info_button
.set_sensitive(count
== 1)
1049 def drag_data_get_data(
1050 self
, treeview
, context
, selection
, target_id
, timestamp
):
1052 treeselection
= treeview
.get_selection()
1053 model
, iter = treeselection
.get_selected()
1054 # only allow moving around top-level parents
1055 if model
.iter_parent(iter) is None:
1056 # send the path of the selected row
1057 data
= model
.get_string_from_iter(iter)
1058 selection
.set(selection
.target
, 8, data
)
1060 self
.__log
.debug("Can't move children...")
1062 def drag_data_recieved(
1063 self
, treeview
, context
, x
, y
, selection
, info
, timestamp
):
1065 drop_info
= treeview
.get_dest_row_at_pos(x
, y
)
1067 # TODO: If user drags the row past the last row, drop_info is None
1068 # I'm not sure if it's safe to simply assume that None is
1069 # euqivalent to the last row...
1070 if None not in [ drop_info
and selection
.data
]:
1071 model
= treeview
.get_model()
1072 path
, position
= drop_info
1074 from_iter
= model
.get_iter_from_string(selection
.data
)
1076 # make sure the to_iter doesn't have a parent
1077 to_iter
= model
.get_iter(path
)
1078 if model
.iter_parent(to_iter
) is not None:
1079 to_iter
= model
.iter_parent(to_iter
)
1081 from_row
= model
.get_path(from_iter
)[0]
1084 if ( position
== gtk
.TREE_VIEW_DROP_BEFORE
or
1085 position
== gtk
.TREE_VIEW_DROP_INTO_OR_BEFORE
):
1086 model
.move_before( from_iter
, to_iter
)
1087 to_row
= to_row
- 1 if from_row
< to_row
else to_row
1088 elif ( position
== gtk
.TREE_VIEW_DROP_AFTER
or
1089 position
== gtk
.TREE_VIEW_DROP_INTO_OR_AFTER
):
1090 model
.move_after( from_iter
, to_iter
)
1091 to_row
= to_row
+ 1 if from_row
> to_row
else to_row
1093 self
.__log
.debug('Drop not supported: %s', position
)
1095 # don't do anything if we're not actually moving rows around
1096 if from_row
!= to_row
:
1097 player
.playlist
.move_item( from_row
, to_row
)
1100 self
.__log
.debug('No drop_data or selection.data available')
1102 def update_model(self
):
1103 plist
= player
.playlist
1104 path_info
= self
.treeview
.get_path_at_pos(0,0)
1105 path
= path_info
[0] if path_info
is not None else None
1107 self
.__model
.clear()
1110 for item
, data
in plist
.get_playlist_item_ids():
1111 parent
= self
.__model
.append(None, (item
, data
.get('title'), None))
1113 for bid
, bname
, bpos
in plist
.get_bookmarks_from_item_id( item
):
1114 nice_bpos
= util
.convert_ns(bpos
)
1115 self
.__model
.append( parent
, (bid
, bname
, nice_bpos
) )
1117 self
.treeview
.expand_all()
1119 if path
is not None:
1120 self
.treeview
.scroll_to_cell(path
)
1122 def label_edited(self
, cellrenderer
, path
, new_text
):
1123 iter = self
.__model
.get_iter(path
)
1124 old_text
= self
.__model
.get_value(iter, 1)
1126 if new_text
.strip() and old_text
!= new_text
:
1127 # this loop will only run once, because only one cell can be
1128 # edited at a time, we use it to get the item and bookmark ids
1129 for m
, bkmk_id
, biter
, item_id
, iiter
in self
.__cur
_selection
():
1130 self
.__model
.set_value(iter, 1, new_text
)
1131 player
.playlist
.update_bookmark(
1132 item_id
, bkmk_id
, name
=new_text
)
1134 self
.__model
.set_value(iter, 1, old_text
)
1136 def on_bookmark_added(self
, parent_id
, bookmark_name
, position
):
1137 util
.notify(_('Bookmark added: %s') % bookmark_name
)
1140 def add_file(self
, widget
):
1141 filename
= get_file_from_filechooser(self
.main
.main_window
)
1142 if filename
is not None:
1143 player
.playlist
.append(filename
)
1145 def add_directory(self
, widget
):
1146 directory
= get_file_from_filechooser(
1147 self
.main
.main_window
, folder
=True )
1148 if directory
is not None:
1149 player
.playlist
.load_directory(directory
, append
=True)
1151 def __cur_selection(self
):
1152 selection
= self
.treeview
.get_selection()
1153 model
, bookmark_paths
= selection
.get_selected_rows()
1155 # Convert the paths to gtk.TreeRowReference objects, because we
1156 # might modify the model while this generator is running
1157 bookmark_refs
= [gtk
.TreeRowReference(model
, p
) for p
in bookmark_paths
]
1159 for reference
in bookmark_refs
:
1160 bookmark_iter
= model
.get_iter(reference
.get_path())
1161 item_iter
= model
.iter_parent(bookmark_iter
)
1163 # bookmark_iter is actually an item_iter
1164 if item_iter
is None:
1165 item_iter
= bookmark_iter
1166 item_id
= model
.get_value(item_iter
, 0)
1167 bookmark_id
, bookmark_iter
= None, None
1169 bookmark_id
= model
.get_value(bookmark_iter
, 0)
1170 item_id
= model
.get_value(item_iter
, 0)
1172 yield model
, bookmark_id
, bookmark_iter
, item_id
, item_iter
1174 def remove_bookmark(self
, w
=None):
1175 for model
, bkmk_id
, bkmk_iter
, item_id
, item_iter
in self
.__cur
_selection
():
1176 player
.playlist
.remove_bookmark( item_id
, bkmk_id
)
1177 if bkmk_iter
is not None:
1178 model
.remove(bkmk_iter
)
1179 elif item_iter
is not None:
1180 model
.remove(item_iter
)
1182 def select_current_item(self
):
1183 model
= self
.treeview
.get_model()
1184 selection
= self
.treeview
.get_selection()
1185 current_item_id
= str(player
.playlist
.get_current_item())
1186 for row
in iter(model
):
1187 if model
.get_value(row
.iter, 0) == current_item_id
:
1188 selection
.unselect_all()
1189 self
.treeview
.set_cursor(row
.path
)
1190 self
.treeview
.scroll_to_cell(row
.path
, use_align
=True)
1193 def show_playlist_item_details(self
, w
):
1194 selection
= self
.treeview
.get_selection()
1195 if selection
.count_selected_rows() == 1:
1196 selected
= self
.__cur
_selection
().next()
1197 model
, bkmk_id
, bkmk_iter
, item_id
, item_iter
= selected
1198 playlist_item
= player
.playlist
.get_item_by_id(item_id
)
1199 PlaylistItemDetails(self
.main
, playlist_item
)
1201 def jump_bookmark(self
, w
):
1202 selected
= list(self
.__cur
_selection
())
1203 if len(selected
) == 1:
1204 # It should be guranteed by the fact that we only enable the
1205 # "Jump to" button when the selection count equals 1.
1206 model
, bkmk_id
, bkmk_iter
, item_id
, item_iter
= selected
.pop(0)
1207 player
.playlist
.load_from_bookmark_id(item_id
, bkmk_id
)
1209 # FIXME: The player/playlist should be able to take care of this
1210 if not player
.playing
:
1214 ##################################################
1215 # PlaylistItemDetails
1216 ##################################################
1217 class PlaylistItemDetails(gtk
.Dialog
):
1218 def __init__(self
, main
, playlist_item
):
1219 gtk
.Dialog
.__init
__( self
, _('Playlist item details'),
1220 main
.main_window
, gtk
.DIALOG_MODAL
,
1221 (gtk
.STOCK_CLOSE
, gtk
.RESPONSE_OK
))
1224 self
.fill(playlist_item
)
1225 self
.set_has_separator(False)
1226 self
.set_resizable(False)
1231 def fill(self
, playlist_item
):
1232 t
= gtk
.Table(10, 2)
1233 self
.vbox
.pack_start(t
, expand
=False)
1235 metadata
= playlist_item
.metadata
1237 t
.attach(gtk
.Label(_('Custom title:')), 0, 1, 0, 1)
1238 t
.attach(gtk
.Label(_('ID:')), 0, 1, 1, 2)
1239 t
.attach(gtk
.Label(_('Playlist ID:')), 0, 1, 2, 3)
1240 t
.attach(gtk
.Label(_('Filepath:')), 0, 1, 3, 4)
1243 for key
in metadata
:
1244 if metadata
[key
] is not None:
1245 t
.attach( gtk
.Label(key
.capitalize()+':'),
1246 0, 1, row_num
, row_num
+1 )
1249 t
.foreach(lambda x
, y
: x
.set_alignment(1, 0.5), None)
1250 t
.foreach(lambda x
, y
: x
.set_markup('<b>%s</b>' % x
.get_label()), None)
1252 t
.attach(gtk
.Label(playlist_item
.title
or _('<not modified>')),1,2,0,1)
1253 t
.attach(gtk
.Label(str(playlist_item
)), 1, 2, 1, 2)
1254 t
.attach(gtk
.Label(playlist_item
.playlist_id
), 1, 2, 2, 3)
1255 t
.attach(gtk
.Label(playlist_item
.filepath
), 1, 2, 3, 4)
1258 for key
in metadata
:
1259 value
= metadata
[key
]
1261 value
= util
.convert_ns(value
)
1262 if metadata
[key
] is not None:
1263 t
.attach( gtk
.Label( str(value
) or _('<not set>')),
1264 1, 2, row_num
, row_num
+1)
1267 t
.foreach(lambda x
, y
: x
.get_alignment() == (0.5, 0.5) and \
1268 x
.set_alignment(0, 0.5), None)
1270 t
.set_border_width(8)
1271 t
.set_row_spacings(4)
1272 t
.set_col_spacings(8)
1274 l
= gtk
.ListStore(str, str)
1276 cr
= gtk
.CellRendererText()
1277 cr
.set_property('ellipsize', pango
.ELLIPSIZE_END
)
1278 c
= gtk
.TreeViewColumn(_('Title'), cr
, text
=0)
1281 c
= gtk
.TreeViewColumn(_('Time'), gtk
.CellRendererText(), text
=1)
1283 playlist_item
.load_bookmarks()
1284 for bookmark
in playlist_item
.bookmarks
:
1285 l
.append([bookmark
.bookmark_name
, \
1286 util
.convert_ns(bookmark
.seek_position
)])
1288 sw
= gtk
.ScrolledWindow()
1289 sw
.set_shadow_type(gtk
.SHADOW_IN
)
1291 sw
.set_policy(gtk
.POLICY_AUTOMATIC
, gtk
.POLICY_AUTOMATIC
)
1292 e
= gtk
.Expander(_('Bookmarks'))
1294 self
.vbox
.pack_start(e
)
1297 def run(filename
=None):
1298 PanucciGUI( filename
)
1301 if __name__
== '__main__':
1302 log
.error( 'Use the "panucci" executable to run this program.' )
1303 log
.error( 'Exiting...' )