Cleanup: Remove commented "if" + unindent
[panucci.git] / src / panucci / main.py
blobd34176c92e8e9e670a8f4871f4a1569e6cdb80a1
1 #!/usr/bin/env python
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
28 import logging
29 import sys
30 import os, os.path
31 import time
33 import gtk
34 import gobject
35 import pango
37 import cgi
38 import dbus
40 import panucci
42 from panucci import widgets
43 from panucci import util
44 from panucci import platform
46 log = logging.getLogger('panucci.panucci')
48 try:
49 import pynotify
50 pynotify.init('Panucci')
51 have_pynotify = True
52 except:
53 have_pynotify = False
55 try:
56 import hildon
57 except:
58 if platform.MAEMO:
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
67 coverart_sizes = {
68 'normal' : 110,
69 'maemo' : 200,
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])
77 locations = [
78 os.path.join(bin_dir, '..', 'share', 'panucci'),
79 os.path.join(bin_dir, '..', 'icons'),
80 '/opt/panucci',
83 for location in locations:
84 fn = os.path.abspath(os.path.join(location, filename))
85 if os.path.exists(fn):
86 return fn
88 def generate_image(filename, is_stock=False):
89 image = None
90 if is_stock:
91 image = gtk.image_new_from_stock(
92 filename, gtk.icon_size_from_name('panucci-button') )
93 else:
94 filename = find_image(filename)
95 if filename is not None:
96 image = gtk.image_new_from_file(filename)
97 if image is not None:
98 if platform.MAEMO:
99 image.set_padding(20, 20)
100 else:
101 image.set_padding(5, 5)
102 image.show()
103 return image
105 def image(widget, filename, is_stock=False):
106 child = widget.get_child()
107 if child is not None:
108 widget.remove(child)
109 image = generate_image(filename, is_stock)
110 if image is not None:
111 widget.add(image)
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 )
134 dlg.set_title(title)
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)
145 response = dlg.run()
146 dlg.destroy()
148 if response == gtk.RESPONSE_YES:
149 return True
150 elif response == gtk.RESPONSE_NO:
151 return False
152 elif response in [gtk.RESPONSE_CANCEL, gtk.RESPONSE_DELETE_EVENT]:
153 return None
155 def get_file_from_filechooser(
156 toplevel_window, folder=False, save_file=False, save_to=None):
158 if folder:
159 open_action = gtk.FILE_CHOOSER_ACTION_SELECT_FOLDER
160 else:
161 open_action = gtk.FILE_CHOOSER_ACTION_OPEN
163 if platform.FREMANTLE:
164 if save_file:
165 dlg = gobject.new(hildon.FileChooserDialog, \
166 action=gtk.FILE_CHOOSER_ACTION_SAVE)
167 else:
168 dlg = gobject.new(hildon.FileChooserDialog, \
169 action=open_action)
170 elif platform.MAEMO:
171 if save_file:
172 args = ( toplevel_window, gtk.FILE_CHOOSER_ACTION_SAVE )
173 else:
174 args = ( toplevel_window, open_action )
176 dlg = hildon.FileChooserDialog( *args )
177 else:
178 if save_file:
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 )) )
183 else:
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()
201 else:
202 filename = None
204 dlg.destroy()
205 return filename
207 def set_stock_button_text( button, text ):
208 alignment = button.get_child()
209 hbox = alignment.get_child()
210 image, label = hbox.get_children()
211 label.set_text(text)
213 ##################################################
214 # PanucciGUI
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)
224 if platform.MAEMO:
225 self.app = hildon.Program()
226 if platform.FREMANTLE:
227 window = hildon.StackableWindow()
228 else:
229 window = hildon.Window()
230 self.app.add_window(window)
231 else:
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()
251 else:
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()
260 if platform.MAEMO:
261 if platform.FREMANTLE:
262 window.set_app_menu(self.create_app_menu())
263 else:
264 window.set_menu(self.create_menu())
265 window.add(self.__player_tab)
266 else:
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)
273 menu_bar.show()
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:
281 print platform.MAEMO
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:
291 player.play()
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':
306 player.pause()
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...
315 # was this better?
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',
337 self.check_queue )
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'))
393 menu.append(b)
395 b = gtk.Button(_('About'))
396 b.connect('clicked', self.about_callback)
397 menu.append(b)
399 menu.show_all()
400 return menu
402 def create_menu(self):
403 # the main menu
404 menu = gtk.Menu()
406 menu_open = gtk.ImageMenuItem(_('Open playlist'))
407 menu_open.set_image(
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'))
420 menu_save.set_image(
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,
442 'progress_locked' )
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)
455 return menu
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)
472 else:
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)
484 notification.show()
485 elif platform.FREMANTLE:
486 hildon.hildon_banner_show_information(self.main_window, \
487 '', message)
488 elif platform.MAEMO:
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()
495 player.quit()
496 gtk.main_quit()
498 def set_progress_indicator(self, loading_title=False):
499 if platform.FREMANTLE:
500 if loading_title:
501 self.main_window.set_title(_('Loading...'))
502 hildon.hildon_gtk_window_set_progress_indicator(self.main_window, \
503 True)
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:
516 response = dialog(
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)
526 if response is None:
527 return False
528 elif response:
529 return self.save_to_playlist_callback()
530 elif not response:
531 return True
532 else:
533 return False
534 else:
535 return True
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' )
555 if filename is None:
556 return False
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'))
566 if response is None:
567 return None
569 elif response:
570 pass
571 elif not response:
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...'))
577 return False
579 return True
581 def __get_fullscreen(self):
582 return self.__window_fullscreen
584 def __set_fullscreen(self, value):
585 if value != self.__window_fullscreen:
586 if value:
587 self.main_window.fullscreen()
588 else:
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):
597 if platform.MAEMO:
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):
605 if notify:
606 filename = os.path.basename(filepath)
607 if success:
608 self.__log.info(
609 self.notify( '%s added successfully.' % filename ))
610 else:
611 self.__log.error(
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,
619 util.about_name,
620 'panucci',
621 panucci.__version__,
622 util.about_text,
623 util.about_copyright,
624 util.about_website,
625 util.about_bugtracker,
626 util.about_donate)
627 else:
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')
638 about.run()
639 about.destroy()
641 def _play_file(self, filename, pause_on_load=False):
642 player.playlist.load( os.path.abspath(filename) )
644 if player.playlist.is_empty:
645 return False
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 ##################################################
658 # PlayerTab
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)
672 # Timers
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)
701 # add a vbox to self
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')
722 else:
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('',
727 update_interval=100,
728 pixel_jump=1,
729 delay_btwn_scrolls=5000,
730 delay_halfway=3000)
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)
744 elif platform.MAEMO:
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:
755 widget2 = None
756 action2 = None
758 return widgets.DualActionButton(widget, action, \
759 widget2, action2, \
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)
810 if platform.MAEMO:
811 self.__gui_root.main_window.connect( 'key-press-event',
812 self.on_key_press )
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"
817 for w in (
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):
843 if platform.MAEMO:
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('')
867 widget.hide()
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):
888 if not 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)
916 if resp:
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 )
929 return True
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 ):
944 if platform.MAEMO:
945 if self.__gui_root.fullscreen:
946 size = coverart_sizes['maemo fullscreen']
947 else:
948 size = coverart_sizes['maemo']
949 else:
950 size = coverart_sizes['normal']
952 return size, size
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 }
963 # set the coverart
964 if tag_message.has_key('image') and tag_message['image'] is not None:
965 value = tag_message['image']
967 pbl = gtk.gdk.PixbufLoader()
968 try:
969 pbl.write(value)
970 pbl.close()
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)
976 except Exception, e:
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():
982 try:
983 tags[tag].set_markup('<big>'+cgi.escape(value)+'</big>')
984 except TypeError, e:
985 self.__log.exception(str(e))
986 tags[tag].set_alignment( 0.5*int(not self.has_coverart), 0.5)
987 tags[tag].show()
989 if tag == 'title':
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)
1003 if resp:
1004 # Preemptively update the progressbar to make seeking smoother
1005 self.set_progress_callback( *resp )
1007 ##################################################
1008 # PlaylistTab
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 )
1020 self.set_spacing(5)
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)
1032 self.update_model()
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)
1066 self.add(sw)
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 )
1116 self.show_all()
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)
1134 else:
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]
1157 to_row = path[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
1167 else:
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 )
1174 else:
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()
1184 # build the tree
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 )
1208 else:
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)
1213 self.update_model()
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
1243 else:
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)
1266 break
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)
1301 self.main = main
1302 self.fill(playlist_item)
1303 self.set_has_separator(False)
1304 self.set_resizable(False)
1305 self.show_all()
1306 self.run()
1307 self.destroy()
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)
1320 row_num = 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 )
1325 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)
1335 row_num = 4
1336 for key in metadata:
1337 value = metadata[key]
1338 if key == 'length':
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)
1343 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)
1353 t = gtk.TreeView(l)
1354 cr = gtk.CellRendererText()
1355 cr.set_property('ellipsize', pango.ELLIPSIZE_END)
1356 c = gtk.TreeViewColumn(_('Title'), cr, text=0)
1357 c.set_expand(True)
1358 t.append_column(c)
1359 c = gtk.TreeViewColumn(_('Time'), gtk.CellRendererText(), text=1)
1360 t.append_column(c)
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)
1368 sw.add(t)
1369 sw.set_policy(gtk.POLICY_AUTOMATIC, gtk.POLICY_AUTOMATIC)
1370 e = gtk.Expander(_('Bookmarks'))
1371 e.add(sw)
1372 if not platform.MAEMO:
1373 self.vbox.pack_start(e)
1375 def run(filename=None):
1376 PanucciGUI(filename)
1377 gtk.main()
1379 if __name__ == '__main__':
1380 log.error( 'Use the "panucci" executable to run this program.' )
1381 log.error( 'Exiting...' )
1382 sys.exit(1)