2 # -*- coding: utf-8 -*-
4 # This file is part of Panucci.
5 # Copyright (c) 2008-2010 The Panucci Audiobook and Podcast Player Project
7 # Panucci is free software: you can redistribute it and/or modify
8 # it under the terms of the GNU General Public License as published by
9 # the Free Software Foundation, either version 3 of the License, or
10 # (at your option) any later version.
12 # Panucci is distributed in the hope that it will be useful,
13 # but WITHOUT ANY WARRANTY; without even the implied warranty of
14 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
15 # GNU General Public License for more details.
17 # You should have received a copy of the GNU General Public License
18 # along with Panucci. If not, see <http://www.gnu.org/licenses/>.
20 # Based on http://thpinfo.com/2008/panucci/:
21 # A resuming media player for Podcasts and Audiobooks
22 # Copyright (c) 2008-05-26 Thomas Perl <thpinfo.com>
23 # (based on http://pygstdocs.berlios.de/pygst-tutorial/seeking.html)
26 from __future__
import absolute_import
42 from panucci
import widgets
43 from panucci
import util
44 from panucci
import platform
46 log
= logging
.getLogger('panucci.panucci')
50 pynotify
.init('Panucci')
59 log
.critical( 'Using GTK widgets, install "python2.5-hildon" '
60 'for this to work properly.' )
62 from panucci
.settings
import settings
63 from panucci
.player
import player
64 from panucci
.dbusinterface
import interface
65 from panucci
.services
import ObservableService
70 'maemo fullscreen' : 275,
73 gtk
.icon_size_register('panucci-button', 32, 32)
75 def find_image(filename
):
76 bin_dir
= os
.path
.dirname(sys
.argv
[0])
78 os
.path
.join(bin_dir
, '..', 'share', 'panucci'),
79 os
.path
.join(bin_dir
, '..', 'icons'),
83 for location
in locations
:
84 fn
= os
.path
.abspath(os
.path
.join(location
, filename
))
85 if os
.path
.exists(fn
):
88 def generate_image(filename
, is_stock
=False):
91 image
= gtk
.image_new_from_stock(
92 filename
, gtk
.icon_size_from_name('panucci-button') )
94 filename
= find_image(filename
)
95 if filename
is not None:
96 image
= gtk
.image_new_from_file(filename
)
99 image
.set_padding(20, 20)
101 image
.set_padding(5, 5)
105 def image(widget
, filename
, is_stock
=False):
106 child
= widget
.get_child()
107 if child
is not None:
109 image
= generate_image(filename
, is_stock
)
110 if image
is not None:
113 def dialog( toplevel_window
, title
, question
, description
,
114 affirmative_button
=gtk
.STOCK_YES
, negative_button
=gtk
.STOCK_NO
,
115 abortion_button
=gtk
.STOCK_CANCEL
):
117 """Present the user with a yes/no/cancel dialog.
118 The return value is either True, False or None, depending on which
119 button has been pressed in the dialog:
121 affirmative button (default: Yes) => True
122 negative button (defaut: No) => False
123 abortion button (default: Cancel) => None
125 When the dialog is closed with the "X" button in the window manager
126 decoration, the return value is always None (same as abortion button).
128 You can set any of the affirmative_button, negative_button or
129 abortion_button values to "None" to hide the corresponding action.
131 dlg
= gtk
.MessageDialog( toplevel_window
, gtk
.DIALOG_MODAL
,
132 gtk
.MESSAGE_QUESTION
, message_format
=question
)
136 if abortion_button
is not None:
137 dlg
.add_button(abortion_button
, gtk
.RESPONSE_CANCEL
)
138 if negative_button
is not None:
139 dlg
.add_button(negative_button
, gtk
.RESPONSE_NO
)
140 if affirmative_button
is not None:
141 dlg
.add_button(affirmative_button
, gtk
.RESPONSE_YES
)
143 dlg
.format_secondary_text(description
)
148 if response
== gtk
.RESPONSE_YES
:
150 elif response
== gtk
.RESPONSE_NO
:
152 elif response
in [gtk
.RESPONSE_CANCEL
, gtk
.RESPONSE_DELETE_EVENT
]:
155 def get_file_from_filechooser(
156 toplevel_window
, folder
=False, save_file
=False, save_to
=None):
159 open_action
= gtk
.FILE_CHOOSER_ACTION_SELECT_FOLDER
161 open_action
= gtk
.FILE_CHOOSER_ACTION_OPEN
163 if platform
.FREMANTLE
:
165 dlg
= gobject
.new(hildon
.FileChooserDialog
, \
166 action
=gtk
.FILE_CHOOSER_ACTION_SAVE
)
168 dlg
= gobject
.new(hildon
.FileChooserDialog
, \
172 args
= ( toplevel_window
, gtk
.FILE_CHOOSER_ACTION_SAVE
)
174 args
= ( toplevel_window
, open_action
)
176 dlg
= hildon
.FileChooserDialog( *args
)
179 args
= ( _('Select file to save playlist to'), None,
180 gtk
.FILE_CHOOSER_ACTION_SAVE
,
181 (( gtk
.STOCK_CANCEL
, gtk
.RESPONSE_REJECT
,
182 gtk
.STOCK_SAVE
, gtk
.RESPONSE_OK
)) )
184 args
= ( _('Select podcast or audiobook'), None, open_action
,
185 (( gtk
.STOCK_CANCEL
, gtk
.RESPONSE_REJECT
,
186 gtk
.STOCK_OPEN
, gtk
.RESPONSE_OK
)) )
188 dlg
= gtk
.FileChooserDialog(*args
)
190 current_folder
= os
.path
.expanduser(settings
.last_folder
)
192 if current_folder
is not None and os
.path
.isdir(current_folder
):
193 dlg
.set_current_folder(current_folder
)
195 if save_file
and save_to
is not None:
196 dlg
.set_current_name(save_to
)
198 if dlg
.run() == gtk
.RESPONSE_OK
:
199 filename
= dlg
.get_filename()
200 settings
.last_folder
= dlg
.get_current_folder()
207 def set_stock_button_text( button
, text
):
208 alignment
= button
.get_child()
209 hbox
= alignment
.get_child()
210 image
, label
= hbox
.get_children()
213 ##################################################
215 ##################################################
216 class PanucciGUI(object):
217 """ The object that holds the entire panucci gui """
219 def __init__(self
, filename
=None):
220 self
.__log
= logging
.getLogger('panucci.panucci.PanucciGUI')
221 interface
.register_gui(self
)
223 # Build the base ui (window and menubar)
225 self
.app
= hildon
.Program()
226 if platform
.FREMANTLE
:
227 window
= hildon
.StackableWindow()
229 window
= hildon
.Window()
230 self
.app
.add_window(window
)
232 window
= gtk
.Window(gtk
.WINDOW_TOPLEVEL
)
234 self
.main_window
= window
235 window
.set_title('Panucci')
236 self
.window_icon
= find_image('panucci.png')
237 if self
.window_icon
is not None:
238 window
.set_icon_from_file( self
.window_icon
)
239 window
.set_default_size(400, -1)
240 window
.set_border_width(0)
241 window
.connect("destroy", self
.destroy
)
243 # Add the tabs (they are private to prevent us from trying to do
244 # something like gui_root.player_tab.some_function() from inside
245 # playlist_tab or vice-versa)
246 self
.__player
_tab
= PlayerTab(self
)
247 self
.__playlist
_tab
= PlaylistTab(self
)
249 if platform
.FREMANTLE
:
250 self
.playlist_window
= hildon
.StackableWindow()
252 self
.playlist_window
= gtk
.Window(gtk
.WINDOW_TOPLEVEL
)
253 self
.playlist_window
.connect('delete-event', gtk
.Widget
.hide_on_delete
)
254 self
.playlist_window
.set_title(_('Playlist'))
255 self
.playlist_window
.set_transient_for(self
.main_window
)
256 self
.playlist_window
.add(self
.__playlist
_tab
)
258 self
.create_actions()
261 if platform
.FREMANTLE
:
262 window
.set_app_menu(self
.create_app_menu())
264 window
.set_menu(self
.create_menu())
265 window
.add(self
.__player
_tab
)
267 menu_vbox
= gtk
.VBox()
268 menu_vbox
.set_spacing(0)
269 window
.add(menu_vbox
)
270 menu_bar
= gtk
.MenuBar()
271 self
.create_desktop_menu(menu_bar
)
272 menu_vbox
.pack_start(menu_bar
, False, False, 0)
274 menu_vbox
.pack_end(self
.__player
_tab
, True, True, 6)
276 # Tie it all together!
277 self
.__ignore
_queue
_check
= False
278 self
.__window
_fullscreen
= False
280 if platform
.MAEMO
and interface
.headset_device
is not None:
282 print interface
.headset_device
283 print 'Enable play/pause with headset button'
284 # Enable play/pause with headset button
285 interface
.headset_device
.connect_to_signal('Condition', self
.handle_headset_button
)
287 # Monitor connection state of BT headset
288 system_bus
= dbus
.SystemBus()
289 def handler_func(device_path
):
290 if device_path
== '/org/freedesktop/Hal/devices/computer_logicaldev_input_1' and settings
.play_on_headset
and not player
.playing
:
292 system_bus
.add_signal_receiver(handler_func
, 'DeviceAdded', 'org.freedesktop.Hal.Manager', None,
293 '/org/freedesktop/Hal/Manager')
294 # End Monitor connection state of BT headset
296 # Monitor BT headset buttons
297 system_bus
= dbus
.SystemBus()
298 def handle_bt_button(signal
, button
):
299 # See https://bugs.maemo.org/show_bug.cgi?id=8283 for details
300 if signal
== 'ButtonPressed':
301 print 'Bluetooth Button pressed'
303 if button
== 'play-cd':
304 player
.play_pause_toggle()
305 elif button
== 'pause-cd':
307 elif button
== 'next-song':
308 #self.seekbutton_callback(None, short_seek)
309 print 'Bluetooth Button FWD pressed'
310 #lambda: self.do_seek(settings.seek_short)
311 #self.do_seek(settings.seek_short)
314 # Helmuth Kadusch: not very elegant, but it works...
316 # self.do_seek(settings.seek_short)
317 # Not working for me...
319 self
.__player
_tab
.do_seek(settings
.seek_short
)
321 elif button
== 'previous-song':
322 # self.seekbutton_callback(None, -1*short_seek)
323 print 'Bluetooth Button REW pressed'
324 # lambda: self.do_seek(-1*settings.seek_short)
325 # self.do_seek(-1*settings.seek_short)
326 self
.__player
_tab
.do_seek(-1*settings
.seek_short
)
329 system_bus
.add_signal_receiver(handle_bt_button
, 'Condition', 'org.freedesktop.Hal.Device', None,
330 '/org/freedesktop/Hal/devices/computer_logicaldev_input_1')
331 # End Monitor BT headset buttons
333 self
.main_window
.connect('key-press-event', self
.on_key_press
)
334 player
.playlist
.register( 'file_queued', self
.on_file_queued
)
336 player
.playlist
.register( 'playlist-to-be-overwritten',
338 self
.__player
_tab
.register( 'select-current-item-request',
339 self
.__select
_current
_item
)
341 self
.main_window
.show_all()
343 # this should be done when the gui is ready
344 player
.init(filepath
=filename
)
346 pos_int
, dur_int
= player
.get_position_duration()
347 # This prevents bogus values from being set while seeking
348 if (pos_int
> 10**9) and (dur_int
> 10**9):
349 self
.set_progress_callback(pos_int
, dur_int
)
351 def create_actions(self
):
352 self
.action_open
= gtk
.Action('open_file', _('Open file'), _('Open a file or playlist'), gtk
.STOCK_OPEN
)
353 self
.action_open
.connect('activate', self
.open_file_callback
)
354 self
.action_open_dir
= gtk
.Action('open_dir', _('Open directory'), _('Open a directory'), gtk
.STOCK_DIRECTORY
)
355 self
.action_open_dir
.connect('activate', self
.open_dir_callback
)
356 self
.action_save
= gtk
.Action('save', _('Save playlist'), _('Save current playlist to file'), gtk
.STOCK_SAVE_AS
)
357 self
.action_save
.connect('activate', self
.save_to_playlist_callback
)
358 self
.action_playlist
= gtk
.Action('playlist', _('Playlist'), _('Open the current playlist'), None)
359 self
.action_playlist
.connect('activate', lambda a
: self
.playlist_window
.show())
360 self
.action_about
= gtk
.Action('about', _('About Panucci'), _('Show application version'), gtk
.STOCK_ABOUT
)
361 self
.action_about
.connect('activate', self
.about_callback
)
362 self
.action_quit
= gtk
.Action('quit', _('Quit'), _('Close Panucci'), gtk
.STOCK_QUIT
)
363 self
.action_quit
.connect('activate', self
.destroy
)
365 def create_desktop_menu(self
, menu_bar
):
366 file_menu_item
= gtk
.MenuItem(_('File'))
367 file_menu
= gtk
.Menu()
368 file_menu
.append(self
.action_open
.create_menu_item())
369 file_menu
.append(self
.action_open_dir
.create_menu_item())
370 file_menu
.append(self
.action_save
.create_menu_item())
371 file_menu
.append(gtk
.SeparatorMenuItem())
372 file_menu
.append(self
.action_quit
.create_menu_item())
373 file_menu_item
.set_submenu(file_menu
)
374 menu_bar
.append(file_menu_item
)
376 tools_menu_item
= gtk
.MenuItem(_('Tools'))
377 tools_menu
= gtk
.Menu()
378 tools_menu
.append(self
.action_playlist
.create_menu_item())
379 tools_menu_item
.set_submenu(tools_menu
)
380 menu_bar
.append(tools_menu_item
)
382 help_menu_item
= gtk
.MenuItem(_('Help'))
383 help_menu
= gtk
.Menu()
384 help_menu
.append(self
.action_about
.create_menu_item())
385 help_menu_item
.set_submenu(help_menu
)
386 menu_bar
.append(help_menu_item
)
388 def create_app_menu(self
):
389 menu
= hildon
.AppMenu()
391 b
= gtk
.Button(_('Playlist'))
392 b
.connect('clicked', lambda b
: self
.__player
_tab
.notify('select-current-item-request'))
395 b
= gtk
.Button(_('About'))
396 b
.connect('clicked', self
.about_callback
)
402 def create_menu(self
):
406 menu_open
= gtk
.ImageMenuItem(_('Open playlist'))
408 gtk
.image_new_from_stock(gtk
.STOCK_OPEN
, gtk
.ICON_SIZE_MENU
))
409 menu_open
.connect("activate", self
.open_file_callback
)
410 menu
.append(menu_open
)
412 # the recent files menu
413 self
.menu_recent
= gtk
.MenuItem(_('Open recent playlist'))
414 menu
.append(self
.menu_recent
)
415 self
.create_recent_files_menu()
417 menu
.append(gtk
.SeparatorMenuItem())
419 menu_save
= gtk
.ImageMenuItem(_('Save current playlist'))
421 gtk
.image_new_from_stock(gtk
.STOCK_SAVE_AS
, gtk
.ICON_SIZE_MENU
))
422 menu_save
.connect("activate", self
.save_to_playlist_callback
)
423 menu
.append(menu_save
)
425 menu
.append(gtk
.SeparatorMenuItem())
427 # the settings sub-menu
428 menu_settings
= gtk
.MenuItem(_('Settings'))
429 menu
.append(menu_settings
)
431 menu_settings_sub
= gtk
.Menu()
432 menu_settings
.set_submenu(menu_settings_sub
)
434 menu_settings_enable_dual_action
= gtk
.CheckMenuItem(
435 _('Enable dual-action buttons') )
436 settings
.attach_checkbutton( menu_settings_enable_dual_action
,
437 'enable_dual_action_btn' )
438 menu_settings_sub
.append(menu_settings_enable_dual_action
)
440 menu_settings_lock_progress
= gtk
.CheckMenuItem(_('Lock Progress Bar'))
441 settings
.attach_checkbutton( menu_settings_lock_progress
,
443 menu_settings_sub
.append(menu_settings_lock_progress
)
445 menu_about
= gtk
.ImageMenuItem(gtk
.STOCK_ABOUT
)
446 menu_about
.connect("activate", self
.about_callback
)
447 menu
.append(menu_about
)
449 menu
.append(gtk
.SeparatorMenuItem())
451 menu_quit
= gtk
.ImageMenuItem(gtk
.STOCK_QUIT
)
452 menu_quit
.connect("activate", self
.destroy
)
453 menu
.append(menu_quit
)
457 def create_recent_files_menu( self
):
458 max_files
= settings
.max_recent_files
459 self
.recent_files
= player
.playlist
.get_recent_files(max_files
)
460 menu_recent_sub
= gtk
.Menu()
462 if len(self
.recent_files
) > 0:
463 for f
in self
.recent_files
:
464 # don't include the temporary playlist in the file list
465 if f
== panucci
.PLAYLIST_FILE
: continue
466 # don't include non-existant files
467 if not os
.path
.exists( f
): continue
468 filename
, extension
= os
.path
.splitext(os
.path
.basename(f
))
469 menu_item
= gtk
.MenuItem( filename
.replace('_', ' '))
470 menu_item
.connect('activate', self
.on_recent_file_activate
, f
)
471 menu_recent_sub
.append(menu_item
)
473 menu_item
= gtk
.MenuItem(_('No recent files available.'))
474 menu_item
.set_sensitive(False)
475 menu_recent_sub
.append(menu_item
)
477 self
.menu_recent
.set_submenu(menu_recent_sub
)
479 def notify(self
, message
):
480 """ Sends a notification using pynotify, returns message """
481 if platform
.DESKTOP
and have_pynotify
:
482 icon
= find_image('panucci_64x64.png')
483 notification
= pynotify
.Notification(self
.main_window
.get_title(), message
, icon
)
485 elif platform
.FREMANTLE
:
486 hildon
.hildon_banner_show_information(self
.main_window
, \
489 # Note: This won't work if we're not in the gtk main loop
490 markup
= '<b>%s</b>\n<small>%s</small>' % (self
.main_window
.get_title(), message
)
491 hildon
.hildon_banner_show_information_with_markup(self
.main_window
, None, markup
)
493 def destroy(self
, widget
):
494 self
.main_window
.hide()
498 def set_progress_indicator(self
, loading_title
=False):
499 if platform
.FREMANTLE
:
501 self
.main_window
.set_title(_('Loading...'))
502 hildon
.hildon_gtk_window_set_progress_indicator(self
.main_window
, \
504 while gtk
.events_pending():
505 gtk
.main_iteration(False)
507 def show_main_window(self
):
508 self
.main_window
.present()
510 def check_queue(self
):
511 """ Makes sure the queue is saved if it has been modified
512 True means a new file can be opened
513 False means the user does not want to continue """
515 if not self
.__ignore
_queue
_check
and player
.playlist
.queue_modified
:
517 self
.main_window
, _('Save current playlist'),
518 _('Current playlist has been modified'),
519 _('Opening a new file will replace the current playlist. ') +
520 _('Do you want to save it before creating a new one?'),
521 affirmative_button
=gtk
.STOCK_SAVE
,
522 negative_button
=_('Discard changes'))
524 self
.__log
.debug('Response to "Save Queue?": %s', response
)
529 return self
.save_to_playlist_callback()
537 def open_file_callback(self
, widget
=None):
538 # set __ingnore__queue_check because we already did the check
539 self
.__ignore
_queue
_check
= True
540 filename
= get_file_from_filechooser(self
.main_window
)
541 if filename
is not None:
542 self
._play
_file
(filename
)
544 self
.__ignore
_queue
_check
= False
546 def open_dir_callback(self
, widget
=None):
547 filename
= get_file_from_filechooser(self
.main_window
, folder
=True)
548 if filename
is not None:
549 self
._play
_file
(filename
)
551 def save_to_playlist_callback(self
, widget
=None):
552 filename
= get_file_from_filechooser(
553 self
.main_window
, save_file
=True, save_to
='playlist.m3u' )
558 if os
.path
.isfile(filename
):
559 response
= dialog( self
.main_window
, _('File already exists'),
560 _('File already exists'),
561 _('The file %s already exists. You can choose another name or '
562 'overwrite the existing file.') % os
.path
.basename(filename
),
563 affirmative_button
=gtk
.STOCK_SAVE
,
564 negative_button
=_('Rename file'))
572 return self
.save_to_playlist_callback()
574 ext
= util
.detect_filetype(filename
)
575 if not player
.playlist
.save_to_new_playlist(filename
, ext
):
576 self
.notify(_('Error saving playlist...'))
581 def __get_fullscreen(self
):
582 return self
.__window
_fullscreen
584 def __set_fullscreen(self
, value
):
585 if value
!= self
.__window
_fullscreen
:
587 self
.main_window
.fullscreen()
589 self
.main_window
.unfullscreen()
591 self
.__window
_fullscreen
= value
592 player
.playlist
.send_metadata()
594 fullscreen
= property( __get_fullscreen
, __set_fullscreen
)
596 def on_key_press(self
, widget
, event
):
598 if event
.keyval
== gtk
.keysyms
.F6
:
599 self
.fullscreen
= not self
.fullscreen
601 def on_recent_file_activate(self
, widget
, filepath
):
602 self
._play
_file
(filepath
)
604 def on_file_queued(self
, filepath
, success
, notify
):
606 filename
= os
.path
.basename(filepath
)
609 self
.notify( '%s added successfully.' % filename
))
612 self
.notify( 'Error adding %s to the queue.' % filename
))
614 def about_callback(self
, widget
):
615 if platform
.FREMANTLE
:
616 from panucci
.aboutdialog
import HeAboutDialog
618 HeAboutDialog
.present(self
.main_window
,
623 util
.about_copyright
,
625 util
.about_bugtracker
,
628 about
= gtk
.AboutDialog()
629 about
.set_transient_for(self
.main_window
)
630 about
.set_name(util
.about_name
)
631 about
.set_version(panucci
.__version
__)
632 about
.set_copyright(util
.about_copyright
)
633 about
.set_comments(util
.about_text
)
634 about
.set_website(util
.about_website
)
635 about
.set_authors(util
.about_authors
)
636 about
.set_translator_credits(_('translator-credits'))
637 about
.set_logo_icon_name('panucci')
641 def _play_file(self
, filename
, pause_on_load
=False):
642 player
.playlist
.load( os
.path
.abspath(filename
) )
644 if player
.playlist
.is_empty
:
647 def handle_headset_button(self
, event
, button
):
648 if event
== 'ButtonPressed' and button
== 'phone':
649 player
.play_pause_toggle()
651 def __select_current_item( self
):
652 # Select the currently playing track in the playlist tab
653 # and switch to it (so we can edit bookmarks, etc.. there)
654 self
.__playlist
_tab
.select_current_item()
655 self
.playlist_window
.show()
657 ##################################################
659 ##################################################
660 class PlayerTab(ObservableService
, gtk
.HBox
):
661 """ The tab that holds the player elements """
663 signals
= [ 'select-current-item-request', ]
665 def __init__(self
, gui_root
):
666 self
.__log
= logging
.getLogger('panucci.panucci.PlayerTab')
667 self
.__gui
_root
= gui_root
669 gtk
.HBox
.__init
__(self
)
670 ObservableService
.__init
__(self
, self
.signals
, self
.__log
)
673 self
.progress_timer_id
= None
675 self
.recent_files
= []
676 self
.make_player_tab()
677 self
.has_coverart
= False
679 #settings.register( 'enable_dual_action_btn_changed',
680 # self.on_dual_action_setting_changed )
681 #settings.register( 'dual_action_button_delay_changed',
682 # self.on_dual_action_setting_changed )
683 #settings.register( 'scrolling_labels_changed', lambda v:
684 # setattr( self.title_label, 'scrolling', v ) )
686 player
.register( 'stopped', self
.on_player_stopped
)
687 player
.register( 'playing', self
.on_player_playing
)
688 player
.register( 'paused', self
.on_player_paused
)
689 player
.playlist
.register( 'end-of-playlist',
690 self
.on_player_end_of_playlist
)
691 player
.playlist
.register( 'new-track-loaded',
692 self
.on_player_new_track
)
693 player
.playlist
.register( 'new-metadata-available',
694 self
.on_player_new_metadata
)
695 player
.playlist
.register( 'reset-playlist',
696 self
.on_player_reset_playlist
)
698 def make_player_tab(self
):
699 main_vbox
= gtk
.VBox()
700 main_vbox
.set_spacing(6)
702 self
.pack_start(main_vbox
, True, True)
704 # a hbox to hold the cover art and metadata vbox
705 metadata_hbox
= gtk
.HBox()
706 metadata_hbox
.set_spacing(6)
707 main_vbox
.pack_start(metadata_hbox
, True, False)
709 self
.cover_art
= gtk
.Image()
710 metadata_hbox
.pack_start( self
.cover_art
, False, False )
712 # vbox to hold metadata
713 metadata_vbox
= gtk
.VBox()
714 metadata_vbox
.pack_start(gtk
.Image(), True, True)
715 self
.artist_label
= gtk
.Label('')
716 self
.artist_label
.set_ellipsize(pango
.ELLIPSIZE_END
)
717 metadata_vbox
.pack_start(self
.artist_label
, False, False)
718 self
.album_label
= gtk
.Label('')
719 if platform
.FREMANTLE
:
720 hildon
.hildon_helper_set_logical_font(self
.album_label
, 'SmallSystemFont')
721 hildon
.hildon_helper_set_logical_color(self
.album_label
, gtk
.RC_FG
, gtk
.STATE_NORMAL
, 'SecondaryTextColor')
723 self
.album_label
.modify_font(pango
.FontDescription('normal 8'))
724 self
.album_label
.set_ellipsize(pango
.ELLIPSIZE_END
)
725 metadata_vbox
.pack_start(self
.album_label
, False, False)
726 self
.title_label
= widgets
.ScrollingLabel('',
729 delay_btwn_scrolls
=5000,
731 self
.title_label
.scrolling
= settings
.scrolling_labels
732 metadata_vbox
.pack_start(self
.title_label
, False, False)
733 metadata_vbox
.pack_start(gtk
.Image(), True, True)
734 metadata_hbox
.pack_start( metadata_vbox
, True, True )
736 progress_eventbox
= gtk
.EventBox()
737 progress_eventbox
.set_events(gtk
.gdk
.BUTTON_PRESS_MASK
)
738 progress_eventbox
.connect(
739 'button-press-event', self
.on_progressbar_changed
)
740 self
.progress
= gtk
.ProgressBar()
741 # make the progress bar more "finger-friendly"
742 if platform
.FREMANTLE
:
743 self
.progress
.set_size_request(-1, 100)
745 self
.progress
.set_size_request(-1, 50)
746 progress_eventbox
.add(self
.progress
)
747 main_vbox
.pack_start( progress_eventbox
, False, False )
749 # make the button box
750 buttonbox
= gtk
.HBox()
752 # A wrapper to help create DualActionButtons with the right settings
753 def create_da(widget
, action
, widget2
=None, action2
=None):
754 if platform
.FREMANTLE
:
758 return widgets
.DualActionButton(widget
, action
, \
760 settings
.dual_action_button_delay
, \
761 settings
.enable_dual_action_btn
)
763 self
.rrewind_button
= create_da(
764 generate_image('media-skip-backward.png'),
765 lambda: self
.do_seek(-1*settings
.seek_long
),
766 generate_image(gtk
.STOCK_GOTO_FIRST
, True),
767 player
.playlist
.prev
)
768 buttonbox
.add(self
.rrewind_button
)
770 self
.rewind_button
= create_da(
771 generate_image('media-seek-backward.png'),
772 lambda: self
.do_seek(-1*settings
.seek_short
))
773 buttonbox
.add(self
.rewind_button
)
775 self
.play_pause_button
= gtk
.Button('')
776 image(self
.play_pause_button
, 'media-playback-start.png')
777 self
.play_pause_button
.connect( 'clicked',
778 self
.on_btn_play_pause_clicked
)
779 self
.play_pause_button
.set_sensitive(False)
780 buttonbox
.add(self
.play_pause_button
)
782 self
.forward_button
= create_da(
783 generate_image('media-seek-forward.png'),
784 lambda: self
.do_seek(settings
.seek_short
))
785 buttonbox
.add(self
.forward_button
)
787 self
.fforward_button
= create_da(
788 generate_image('media-skip-forward.png'),
789 lambda: self
.do_seek(settings
.seek_long
),
790 generate_image(gtk
.STOCK_GOTO_LAST
, True),
791 player
.playlist
.next
)
792 buttonbox
.add(self
.fforward_button
)
794 self
.bookmarks_button
= create_da(
795 generate_image('bookmark-new.png'),
796 player
.add_bookmark_at_current_position
,
797 generate_image(gtk
.STOCK_JUMP_TO
, True),
798 lambda *args
: self
.notify('select-current-item-request'))
799 buttonbox
.add(self
.bookmarks_button
)
800 self
.set_controls_sensitivity(False)
802 if platform
.FREMANTLE
:
803 for child
in buttonbox
.get_children():
804 if isinstance(child
, gtk
.Button
):
805 child
.set_name('HildonButton-thumb')
806 buttonbox
.set_size_request(-1, 105)
808 main_vbox
.pack_start(buttonbox
, False, False)
811 self
.__gui
_root
.main_window
.connect( 'key-press-event',
814 # Disable focus for all widgets, so we can use the cursor
815 # keys + enter to directly control our media player, which
816 # is handled by "key-press-event"
818 self
.rrewind_button
, self
.rewind_button
,
819 self
.play_pause_button
, self
.forward_button
,
820 self
.fforward_button
, self
.progress
,
821 self
.bookmarks_button
, ):
822 w
.unset_flags(gtk
.CAN_FOCUS
)
824 def set_controls_sensitivity(self
, sensitive
):
825 for button
in self
.forward_button
, self
.rewind_button
, \
826 self
.fforward_button
, self
.rrewind_button
:
828 button
.set_sensitive(sensitive
)
830 # the play/pause button should always be available except
831 # for when the player starts without a file
832 self
.play_pause_button
.set_sensitive(True)
834 def on_dual_action_setting_changed( self
, *args
):
835 for button
in self
.forward_button
, self
.rewind_button
, \
836 self
.fforward_button
, self
.rrewind_button
, \
837 self
.bookmarks_button
:
839 button
.set_longpress_enabled( settings
.enable_dual_action_btn
)
840 button
.set_duration( settings
.dual_action_button_delay
)
842 def on_key_press(self
, widget
, event
):
844 if event
.keyval
== gtk
.keysyms
.Left
: # seek back
845 self
.do_seek( -1 * settings
.seek_long
)
846 elif event
.keyval
== gtk
.keysyms
.Right
: # seek forward
847 self
.do_seek( settings
.seek_long
)
848 elif event
.keyval
== gtk
.keysyms
.Return
: # play/pause
849 self
.on_btn_play_pause_clicked()
851 def on_player_stopped(self
):
852 self
.stop_progress_timer()
853 self
.set_controls_sensitivity(False)
854 image(self
.play_pause_button
, 'media-playback-start.png')
856 def on_player_playing(self
):
857 self
.start_progress_timer()
858 image(self
.play_pause_button
, 'media-playback-pause.png')
859 self
.set_controls_sensitivity(True)
860 if platform
.FREMANTLE
:
861 hildon
.hildon_gtk_window_set_progress_indicator(\
862 self
.__gui
_root
.main_window
, False)
864 def on_player_new_track(self
):
865 for widget
in [self
.title_label
,self
.artist_label
,self
.album_label
]:
866 widget
.set_markup('')
869 self
.cover_art
.hide()
870 self
.has_coverart
= False
872 def on_player_new_metadata(self
):
873 self
.metadata
= player
.playlist
.get_file_metadata()
874 self
.set_metadata(self
.metadata
)
876 if not player
.playing
:
877 position
= player
.playlist
.get_current_position()
878 estimated_length
= self
.metadata
.get('length', 0)
879 self
.set_progress_callback( position
, estimated_length
)
880 player
.set_position_duration(position
, 0)
882 def on_player_paused( self
, position
, duration
):
883 self
.stop_progress_timer() # This should save some power
884 self
.set_progress_callback( position
, duration
)
885 image(self
.play_pause_button
, 'media-playback-start.png')
887 def on_player_end_of_playlist(self
, loop
):
889 player
.stop_end_of_playlist()
890 estimated_length
= self
.metadata
.get('length', 0)
891 self
.set_progress_callback( 0, estimated_length
)
892 player
.set_position_duration(0, 0)
894 def on_player_reset_playlist(self
):
895 self
.on_player_stopped()
896 self
.on_player_new_track()
897 self
.reset_progress()
899 def reset_progress(self
):
900 self
.progress
.set_fraction(0)
901 self
.set_progress_callback(0,0)
903 def set_progress_callback(self
, time_elapsed
, total_time
):
904 """ times must be in nanoseconds """
905 time_string
= "%s / %s" % ( util
.convert_ns(time_elapsed
),
906 util
.convert_ns(total_time
) )
907 self
.progress
.set_text( time_string
)
908 fraction
= float(time_elapsed
) / float(total_time
) if total_time
else 0
909 self
.progress
.set_fraction( fraction
)
911 def on_progressbar_changed(self
, widget
, event
):
912 if ( not settings
.progress_locked
and
913 event
.type == gtk
.gdk
.BUTTON_PRESS
and event
.button
== 1 ):
914 new_fraction
= event
.x
/float(widget
.get_allocation().width
)
915 resp
= player
.do_seek(percent
=new_fraction
)
917 # Preemptively update the progressbar to make seeking smoother
918 self
.set_progress_callback( *resp
)
920 def on_btn_play_pause_clicked(self
, widget
=None):
921 player
.play_pause_toggle()
923 def progress_timer_callback( self
):
924 if player
.playing
and not player
.seeking
:
925 pos_int
, dur_int
= player
.get_position_duration()
926 # This prevents bogus values from being set while seeking
927 if ( pos_int
> 10**9 ) and ( dur_int
> 10**9 ):
928 self
.set_progress_callback( pos_int
, dur_int
)
931 def start_progress_timer( self
):
932 if self
.progress_timer_id
is not None:
933 self
.stop_progress_timer()
935 self
.progress_timer_id
= gobject
.timeout_add(
936 1000, self
.progress_timer_callback
)
938 def stop_progress_timer( self
):
939 if self
.progress_timer_id
is not None:
940 gobject
.source_remove( self
.progress_timer_id
)
941 self
.progress_timer_id
= None
943 def get_coverart_size( self
):
945 if self
.__gui
_root
.fullscreen
:
946 size
= coverart_sizes
['maemo fullscreen']
948 size
= coverart_sizes
['maemo']
950 size
= coverart_sizes
['normal']
954 def set_coverart( self
, pixbuf
):
955 self
.cover_art
.set_from_pixbuf(pixbuf
)
956 self
.cover_art
.show()
957 self
.has_coverart
= True
959 def set_metadata( self
, tag_message
):
960 tags
= { 'title': self
.title_label
, 'artist': self
.artist_label
,
961 'album': self
.album_label
}
964 if tag_message
.has_key('image') and tag_message
['image'] is not None:
965 value
= tag_message
['image']
967 pbl
= gtk
.gdk
.PixbufLoader()
972 x
, y
= self
.get_coverart_size()
973 pixbuf
= pbl
.get_pixbuf()
974 pixbuf
= pixbuf
.scale_simple( x
, y
, gtk
.gdk
.INTERP_BILINEAR
)
975 self
.set_coverart(pixbuf
)
977 self
.__log
.exception('Error setting coverart...')
979 # set the text metadata
980 for tag
,value
in tag_message
.iteritems():
981 if tags
.has_key(tag
) and value
is not None and value
.strip():
983 tags
[tag
].set_markup('<big>'+cgi
.escape(value
)+'</big>')
985 self
.__log
.exception(str(e
))
986 tags
[tag
].set_alignment( 0.5*int(not self
.has_coverart
), 0.5)
990 # make the title bold
991 tags
[tag
].set_markup('<b><big>'+cgi
.escape(value
)+'</big></b>')
993 if not platform
.MAEMO
:
994 value
+= ' - Panucci'
996 if platform
.FREMANTLE
and len(value
) > 25:
997 value
= value
[:24] + '...'
999 self
.__gui
_root
.main_window
.set_title( value
)
1001 def do_seek(self
, seek_amount
):
1002 resp
= player
.do_seek(from_current
=seek_amount
*10**9)
1004 # Preemptively update the progressbar to make seeking smoother
1005 self
.set_progress_callback( *resp
)
1007 ##################################################
1009 ##################################################
1010 class PlaylistTab(gtk
.VBox
):
1011 def __init__(self
, main_window
):
1012 gtk
.VBox
.__init
__(self
)
1013 self
.__log
= logging
.getLogger('panucci.panucci.BookmarksWindow')
1014 self
.main
= main_window
1016 self
.__model
= gtk
.TreeStore(
1017 # uid, name, position
1018 gobject
.TYPE_STRING
, gobject
.TYPE_STRING
, gobject
.TYPE_STRING
)
1021 self
.treeview
= gtk
.TreeView()
1022 self
.treeview
.set_model(self
.__model
)
1023 self
.treeview
.set_headers_visible(True)
1024 tree_selection
= self
.treeview
.get_selection()
1025 # This breaks drag and drop, only use single selection for now
1026 # tree_selection.set_mode(gtk.SELECTION_MULTIPLE)
1027 tree_selection
.connect('changed', self
.tree_selection_changed
)
1029 # The tree lines look nasty on maemo
1030 if platform
.DESKTOP
:
1031 self
.treeview
.set_enable_tree_lines(True)
1034 ncol
= gtk
.TreeViewColumn(_('Name'))
1035 ncell
= gtk
.CellRendererText()
1036 ncell
.set_property('ellipsize', pango
.ELLIPSIZE_END
)
1037 ncell
.set_property('editable', True)
1038 ncell
.connect('edited', self
.label_edited
)
1039 ncol
.set_expand(True)
1040 ncol
.pack_start(ncell
)
1041 ncol
.add_attribute(ncell
, 'text', 1)
1043 tcol
= gtk
.TreeViewColumn(_('Position'))
1044 tcell
= gtk
.CellRendererText()
1045 tcol
.pack_start(tcell
)
1046 tcol
.add_attribute(tcell
, 'text', 2)
1048 self
.treeview
.append_column(ncol
)
1049 self
.treeview
.append_column(tcol
)
1050 self
.treeview
.connect('drag-data-received', self
.drag_data_recieved
)
1051 self
.treeview
.connect('drag_data_get', self
.drag_data_get_data
)
1053 treeview_targets
= [
1054 ( 'playlist_row_data', gtk
.TARGET_SAME_WIDGET
, 0 ) ]
1056 self
.treeview
.enable_model_drag_source(
1057 gtk
.gdk
.BUTTON1_MASK
, treeview_targets
, gtk
.gdk
.ACTION_COPY
)
1059 self
.treeview
.enable_model_drag_dest(
1060 treeview_targets
, gtk
.gdk
.ACTION_COPY
)
1062 sw
= gtk
.ScrolledWindow()
1063 sw
.set_policy(gtk
.POLICY_AUTOMATIC
, gtk
.POLICY_AUTOMATIC
)
1064 sw
.set_shadow_type(gtk
.SHADOW_IN
)
1065 sw
.add(self
.treeview
)
1068 self
.hbox
= gtk
.HBox()
1070 self
.add_button
= gtk
.Button(gtk
.STOCK_NEW
)
1071 self
.add_button
.set_use_stock(True)
1072 set_stock_button_text( self
.add_button
, _('Add File') )
1073 self
.add_button
.connect('clicked', self
.add_file
)
1074 self
.hbox
.pack_start(self
.add_button
, True, True)
1076 self
.dir_button
= gtk
.Button(gtk
.STOCK_OPEN
)
1077 self
.dir_button
.set_use_stock(True)
1078 set_stock_button_text( self
.dir_button
, _('Add Directory') )
1079 self
.dir_button
.connect('clicked', self
.add_directory
)
1080 self
.hbox
.pack_start(self
.dir_button
, True, True)
1082 self
.remove_button
= gtk
.Button(gtk
.STOCK_REMOVE
)
1083 self
.remove_button
.set_use_stock(True)
1084 self
.remove_button
.connect('clicked', self
.remove_bookmark
)
1085 self
.hbox
.pack_start(self
.remove_button
, True, True)
1087 self
.jump_button
= gtk
.Button(gtk
.STOCK_JUMP_TO
)
1088 self
.jump_button
.set_use_stock(True)
1089 self
.jump_button
.connect('clicked', self
.jump_bookmark
)
1090 self
.hbox
.pack_start(self
.jump_button
, True, True)
1092 self
.info_button
= gtk
.Button()
1093 self
.info_button
.add(
1094 gtk
.image_new_from_stock(gtk
.STOCK_INFO
, gtk
.ICON_SIZE_BUTTON
))
1095 self
.info_button
.connect('clicked', self
.show_playlist_item_details
)
1096 self
.hbox
.pack_start(self
.info_button
, True, True)
1098 self
.empty_button
= gtk
.Button()
1099 self
.empty_button
.add(
1100 gtk
.image_new_from_stock(gtk
.STOCK_DELETE
, gtk
.ICON_SIZE_BUTTON
))
1101 self
.empty_button
.connect('clicked', self
.empty_bookmark
)
1102 self
.hbox
.pack_start(self
.empty_button
, True, True)
1104 if platform
.FREMANTLE
:
1105 for child
in self
.hbox
.get_children():
1106 if isinstance(child
, gtk
.Button
):
1107 child
.set_name('HildonButton-thumb')
1108 self
.hbox
.set_size_request(-1, 105)
1110 self
.pack_start(self
.hbox
, False, True)
1112 player
.playlist
.register( 'file_queued',
1113 lambda x
,y
,z
: self
.update_model() )
1114 player
.playlist
.register( 'bookmark_added', self
.on_bookmark_added
)
1118 def tree_selection_changed(self
, treeselection
):
1119 count
= treeselection
.count_selected_rows()
1120 self
.remove_button
.set_sensitive(count
> 0)
1121 self
.jump_button
.set_sensitive(count
== 1)
1122 self
.info_button
.set_sensitive(count
== 1)
1124 def drag_data_get_data(
1125 self
, treeview
, context
, selection
, target_id
, timestamp
):
1127 treeselection
= treeview
.get_selection()
1128 model
, iter = treeselection
.get_selected()
1129 # only allow moving around top-level parents
1130 if model
.iter_parent(iter) is None:
1131 # send the path of the selected row
1132 data
= model
.get_string_from_iter(iter)
1133 selection
.set(selection
.target
, 8, data
)
1135 self
.__log
.debug("Can't move children...")
1137 def drag_data_recieved(
1138 self
, treeview
, context
, x
, y
, selection
, info
, timestamp
):
1140 drop_info
= treeview
.get_dest_row_at_pos(x
, y
)
1142 # TODO: If user drags the row past the last row, drop_info is None
1143 # I'm not sure if it's safe to simply assume that None is
1144 # euqivalent to the last row...
1145 if None not in [ drop_info
and selection
.data
]:
1146 model
= treeview
.get_model()
1147 path
, position
= drop_info
1149 from_iter
= model
.get_iter_from_string(selection
.data
)
1151 # make sure the to_iter doesn't have a parent
1152 to_iter
= model
.get_iter(path
)
1153 if model
.iter_parent(to_iter
) is not None:
1154 to_iter
= model
.iter_parent(to_iter
)
1156 from_row
= model
.get_path(from_iter
)[0]
1159 if ( position
== gtk
.TREE_VIEW_DROP_BEFORE
or
1160 position
== gtk
.TREE_VIEW_DROP_INTO_OR_BEFORE
):
1161 model
.move_before( from_iter
, to_iter
)
1162 to_row
= to_row
- 1 if from_row
< to_row
else to_row
1163 elif ( position
== gtk
.TREE_VIEW_DROP_AFTER
or
1164 position
== gtk
.TREE_VIEW_DROP_INTO_OR_AFTER
):
1165 model
.move_after( from_iter
, to_iter
)
1166 to_row
= to_row
+ 1 if from_row
> to_row
else to_row
1168 self
.__log
.debug('Drop not supported: %s', position
)
1170 # don't do anything if we're not actually moving rows around
1171 if from_row
!= to_row
:
1172 player
.playlist
.move_item( from_row
, to_row
)
1175 self
.__log
.debug('No drop_data or selection.data available')
1177 def update_model(self
):
1178 plist
= player
.playlist
1179 path_info
= self
.treeview
.get_path_at_pos(0,0)
1180 path
= path_info
[0] if path_info
is not None else None
1182 self
.__model
.clear()
1185 for item
, data
in plist
.get_playlist_item_ids():
1186 parent
= self
.__model
.append(None, (item
, data
.get('title'), None))
1188 for bid
, bname
, bpos
in plist
.get_bookmarks_from_item_id( item
):
1189 nice_bpos
= util
.convert_ns(bpos
)
1190 self
.__model
.append( parent
, (bid
, bname
, nice_bpos
) )
1192 self
.treeview
.expand_all()
1194 if path
is not None:
1195 self
.treeview
.scroll_to_cell(path
)
1197 def label_edited(self
, cellrenderer
, path
, new_text
):
1198 iter = self
.__model
.get_iter(path
)
1199 old_text
= self
.__model
.get_value(iter, 1)
1201 if new_text
.strip() and old_text
!= new_text
:
1202 # this loop will only run once, because only one cell can be
1203 # edited at a time, we use it to get the item and bookmark ids
1204 for m
, bkmk_id
, biter
, item_id
, iiter
in self
.__cur
_selection
():
1205 self
.__model
.set_value(iter, 1, new_text
)
1206 player
.playlist
.update_bookmark(
1207 item_id
, bkmk_id
, name
=new_text
)
1209 self
.__model
.set_value(iter, 1, old_text
)
1211 def on_bookmark_added(self
, parent_id
, bookmark_name
, position
):
1212 self
.main
.notify(_('Bookmark added: %s') % bookmark_name
)
1215 def add_file(self
, widget
):
1216 filename
= get_file_from_filechooser(self
.main
.main_window
)
1217 if filename
is not None:
1218 player
.playlist
.load(filename
)
1220 def add_directory(self
, widget
):
1221 directory
= get_file_from_filechooser(
1222 self
.main
.main_window
, folder
=True )
1223 if directory
is not None:
1224 player
.playlist
.load(directory
)
1226 def __cur_selection(self
):
1227 selection
= self
.treeview
.get_selection()
1228 model
, bookmark_paths
= selection
.get_selected_rows()
1230 # Convert the paths to gtk.TreeRowReference objects, because we
1231 # might modify the model while this generator is running
1232 bookmark_refs
= [gtk
.TreeRowReference(model
, p
) for p
in bookmark_paths
]
1234 for reference
in bookmark_refs
:
1235 bookmark_iter
= model
.get_iter(reference
.get_path())
1236 item_iter
= model
.iter_parent(bookmark_iter
)
1238 # bookmark_iter is actually an item_iter
1239 if item_iter
is None:
1240 item_iter
= bookmark_iter
1241 item_id
= model
.get_value(item_iter
, 0)
1242 bookmark_id
, bookmark_iter
= None, None
1244 bookmark_id
= model
.get_value(bookmark_iter
, 0)
1245 item_id
= model
.get_value(item_iter
, 0)
1247 yield model
, bookmark_id
, bookmark_iter
, item_id
, item_iter
1249 def remove_bookmark(self
, w
=None):
1250 for model
, bkmk_id
, bkmk_iter
, item_id
, item_iter
in self
.__cur
_selection
():
1251 player
.playlist
.remove_bookmark( item_id
, bkmk_id
)
1252 if bkmk_iter
is not None:
1253 model
.remove(bkmk_iter
)
1254 elif item_iter
is not None:
1255 model
.remove(item_iter
)
1257 def select_current_item(self
):
1258 model
= self
.treeview
.get_model()
1259 selection
= self
.treeview
.get_selection()
1260 current_item_id
= str(player
.playlist
.get_current_item())
1261 for row
in iter(model
):
1262 if model
.get_value(row
.iter, 0) == current_item_id
:
1263 selection
.unselect_all()
1264 self
.treeview
.set_cursor(row
.path
)
1265 self
.treeview
.scroll_to_cell(row
.path
, use_align
=True)
1268 def show_playlist_item_details(self
, w
):
1269 selection
= self
.treeview
.get_selection()
1270 if selection
.count_selected_rows() == 1:
1271 selected
= self
.__cur
_selection
().next()
1272 model
, bkmk_id
, bkmk_iter
, item_id
, item_iter
= selected
1273 playlist_item
= player
.playlist
.get_item_by_id(item_id
)
1274 PlaylistItemDetails(self
.main
, playlist_item
)
1276 def jump_bookmark(self
, w
):
1277 selected
= list(self
.__cur
_selection
())
1278 if len(selected
) == 1:
1279 # It should be guranteed by the fact that we only enable the
1280 # "Jump to" button when the selection count equals 1.
1281 model
, bkmk_id
, bkmk_iter
, item_id
, item_iter
= selected
.pop(0)
1282 player
.playlist
.load_from_bookmark_id(item_id
, bkmk_id
)
1284 # FIXME: The player/playlist should be able to take care of this
1286 def empty_bookmark(self
, w
):
1287 player
.playlist
.reset_playlist()
1288 self
.treeview
.get_model().clear()
1290 ##################################################
1291 # PlaylistItemDetails
1292 ##################################################
1293 class PlaylistItemDetails(gtk
.Dialog
):
1294 def __init__(self
, main
, playlist_item
):
1295 gtk
.Dialog
.__init
__(self
, _('Playlist item details'),
1296 main
.main_window
, gtk
.DIALOG_MODAL
)
1298 if not platform
.FREMANTLE
:
1299 self
.add_button(gtk
.STOCK_CLOSE
, gtk
.RESPONSE_OK
)
1302 self
.fill(playlist_item
)
1303 self
.set_has_separator(False)
1304 self
.set_resizable(False)
1309 def fill(self
, playlist_item
):
1310 t
= gtk
.Table(10, 2)
1311 self
.vbox
.pack_start(t
, expand
=False)
1313 metadata
= playlist_item
.metadata
1315 t
.attach(gtk
.Label(_('Custom title:')), 0, 1, 0, 1)
1316 t
.attach(gtk
.Label(_('ID:')), 0, 1, 1, 2)
1317 t
.attach(gtk
.Label(_('Playlist ID:')), 0, 1, 2, 3)
1318 t
.attach(gtk
.Label(_('Filepath:')), 0, 1, 3, 4)
1321 for key
in metadata
:
1322 if metadata
[key
] is not None:
1323 t
.attach( gtk
.Label(key
.capitalize()+':'),
1324 0, 1, row_num
, row_num
+1 )
1327 t
.foreach(lambda x
, y
: x
.set_alignment(1, 0.5), None)
1328 t
.foreach(lambda x
, y
: x
.set_markup('<b>%s</b>' % x
.get_label()), None)
1330 t
.attach(gtk
.Label(playlist_item
.title
or _('<not modified>')),1,2,0,1)
1331 t
.attach(gtk
.Label(str(playlist_item
)), 1, 2, 1, 2)
1332 t
.attach(gtk
.Label(playlist_item
.playlist_id
), 1, 2, 2, 3)
1333 t
.attach(gtk
.Label(playlist_item
.filepath
), 1, 2, 3, 4)
1336 for key
in metadata
:
1337 value
= metadata
[key
]
1339 value
= util
.convert_ns(value
)
1340 if metadata
[key
] is not None:
1341 t
.attach( gtk
.Label( str(value
) or _('<not set>')),
1342 1, 2, row_num
, row_num
+1)
1345 t
.foreach(lambda x
, y
: x
.get_alignment() == (0.5, 0.5) and \
1346 x
.set_alignment(0, 0.5), None)
1348 t
.set_border_width(8)
1349 t
.set_row_spacings(4)
1350 t
.set_col_spacings(8)
1352 l
= gtk
.ListStore(str, str)
1354 cr
= gtk
.CellRendererText()
1355 cr
.set_property('ellipsize', pango
.ELLIPSIZE_END
)
1356 c
= gtk
.TreeViewColumn(_('Title'), cr
, text
=0)
1359 c
= gtk
.TreeViewColumn(_('Time'), gtk
.CellRendererText(), text
=1)
1361 playlist_item
.load_bookmarks()
1362 for bookmark
in playlist_item
.bookmarks
:
1363 l
.append([bookmark
.bookmark_name
, \
1364 util
.convert_ns(bookmark
.seek_position
)])
1366 sw
= gtk
.ScrolledWindow()
1367 sw
.set_shadow_type(gtk
.SHADOW_IN
)
1369 sw
.set_policy(gtk
.POLICY_AUTOMATIC
, gtk
.POLICY_AUTOMATIC
)
1370 e
= gtk
.Expander(_('Bookmarks'))
1372 if not platform
.MAEMO
:
1373 self
.vbox
.pack_start(e
)
1375 def run(filename
=None):
1376 PanucciGUI(filename
)
1379 if __name__
== '__main__':
1380 log
.error( 'Use the "panucci" executable to run this program.' )
1381 log
.error( 'Exiting...' )