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
67 about_name
= 'Panucci'
68 about_text
= _('Resuming audiobook and podcast player')
69 about_authors
= ['Thomas Perl', 'Nick (nikosapi)', 'Matthew Taylor']
70 about_website
= 'http://gpodder.org/panucci/'
71 about_bugtracker
= 'http://bugs.maemo.org/enter_bug.cgi?product=Panucci'
72 about_donate
= 'http://gpodder.org/donate'
73 about_copyright
= '© 2008-2010 Thomas Perl and the Panucci Team'
78 'maemo fullscreen' : 275,
81 gtk
.icon_size_register('panucci-button', 32, 32)
83 def find_image(filename
):
84 bin_dir
= os
.path
.dirname(sys
.argv
[0])
86 os
.path
.join(bin_dir
, '..', 'share', 'panucci'),
87 os
.path
.join(bin_dir
, '..', 'icons'),
91 for location
in locations
:
92 fn
= os
.path
.abspath(os
.path
.join(location
, filename
))
93 if os
.path
.exists(fn
):
96 def generate_image(filename
, is_stock
=False):
99 image
= gtk
.image_new_from_stock(
100 filename
, gtk
.icon_size_from_name('panucci-button') )
102 filename
= find_image(filename
)
103 if filename
is not None:
104 image
= gtk
.image_new_from_file(filename
)
105 if image
is not None:
107 image
.set_padding(20, 20)
109 image
.set_padding(5, 5)
113 def image(widget
, filename
, is_stock
=False):
114 child
= widget
.get_child()
115 if child
is not None:
117 image
= generate_image(filename
, is_stock
)
118 if image
is not None:
121 def dialog( toplevel_window
, title
, question
, description
,
122 affirmative_button
=gtk
.STOCK_YES
, negative_button
=gtk
.STOCK_NO
,
123 abortion_button
=gtk
.STOCK_CANCEL
):
125 """Present the user with a yes/no/cancel dialog.
126 The return value is either True, False or None, depending on which
127 button has been pressed in the dialog:
129 affirmative button (default: Yes) => True
130 negative button (defaut: No) => False
131 abortion button (default: Cancel) => None
133 When the dialog is closed with the "X" button in the window manager
134 decoration, the return value is always None (same as abortion button).
136 You can set any of the affirmative_button, negative_button or
137 abortion_button values to "None" to hide the corresponding action.
139 dlg
= gtk
.MessageDialog( toplevel_window
, gtk
.DIALOG_MODAL
,
140 gtk
.MESSAGE_QUESTION
, message_format
=question
)
144 if abortion_button
is not None:
145 dlg
.add_button(abortion_button
, gtk
.RESPONSE_CANCEL
)
146 if negative_button
is not None:
147 dlg
.add_button(negative_button
, gtk
.RESPONSE_NO
)
148 if affirmative_button
is not None:
149 dlg
.add_button(affirmative_button
, gtk
.RESPONSE_YES
)
151 dlg
.format_secondary_text(description
)
156 if response
== gtk
.RESPONSE_YES
:
158 elif response
== gtk
.RESPONSE_NO
:
160 elif response
in [gtk
.RESPONSE_CANCEL
, gtk
.RESPONSE_DELETE_EVENT
]:
163 def get_file_from_filechooser(
164 toplevel_window
, folder
=False, save_file
=False, save_to
=None):
167 open_action
= gtk
.FILE_CHOOSER_ACTION_SELECT_FOLDER
169 open_action
= gtk
.FILE_CHOOSER_ACTION_OPEN
171 if platform
.FREMANTLE
:
173 dlg
= gobject
.new(hildon
.FileChooserDialog
, \
174 action
=gtk
.FILE_CHOOSER_ACTION_SAVE
)
176 dlg
= gobject
.new(hildon
.FileChooserDialog
, \
180 args
= ( toplevel_window
, gtk
.FILE_CHOOSER_ACTION_SAVE
)
182 args
= ( toplevel_window
, open_action
)
184 dlg
= hildon
.FileChooserDialog( *args
)
187 args
= ( _('Select file to save playlist to'), None,
188 gtk
.FILE_CHOOSER_ACTION_SAVE
,
189 (( gtk
.STOCK_CANCEL
, gtk
.RESPONSE_REJECT
,
190 gtk
.STOCK_SAVE
, gtk
.RESPONSE_OK
)) )
192 args
= ( _('Select podcast or audiobook'), None, open_action
,
193 (( gtk
.STOCK_CANCEL
, gtk
.RESPONSE_REJECT
,
194 gtk
.STOCK_OPEN
, gtk
.RESPONSE_OK
)) )
196 dlg
= gtk
.FileChooserDialog(*args
)
198 current_folder
= os
.path
.expanduser(settings
.last_folder
)
200 if current_folder
is not None and os
.path
.isdir(current_folder
):
201 dlg
.set_current_folder(current_folder
)
203 if save_file
and save_to
is not None:
204 dlg
.set_current_name(save_to
)
206 if dlg
.run() == gtk
.RESPONSE_OK
:
207 filename
= dlg
.get_filename()
208 settings
.last_folder
= dlg
.get_current_folder()
215 def set_stock_button_text( button
, text
):
216 alignment
= button
.get_child()
217 hbox
= alignment
.get_child()
218 image
, label
= hbox
.get_children()
221 ##################################################
223 ##################################################
224 class PanucciGUI(object):
225 """ The object that holds the entire panucci gui """
227 def __init__(self
, filename
=None):
228 self
.__log
= logging
.getLogger('panucci.panucci.PanucciGUI')
229 interface
.register_gui(self
)
231 # Build the base ui (window and menubar)
233 self
.app
= hildon
.Program()
234 if platform
.FREMANTLE
:
235 window
= hildon
.StackableWindow()
237 window
= hildon
.Window()
238 self
.app
.add_window(window
)
240 window
= gtk
.Window(gtk
.WINDOW_TOPLEVEL
)
242 self
.main_window
= window
243 window
.set_title('Panucci')
244 self
.window_icon
= find_image('panucci.png')
245 if self
.window_icon
is not None:
246 window
.set_icon_from_file( self
.window_icon
)
247 window
.set_default_size(400, -1)
248 window
.set_border_width(0)
249 window
.connect("destroy", self
.destroy
)
251 # Add the tabs (they are private to prevent us from trying to do
252 # something like gui_root.player_tab.some_function() from inside
253 # playlist_tab or vice-versa)
254 self
.__player
_tab
= PlayerTab(self
)
255 self
.__playlist
_tab
= PlaylistTab(self
)
257 if platform
.FREMANTLE
:
258 self
.playlist_window
= hildon
.StackableWindow()
260 self
.playlist_window
= gtk
.Window(gtk
.WINDOW_TOPLEVEL
)
261 self
.playlist_window
.connect('delete-event', gtk
.Widget
.hide_on_delete
)
262 self
.playlist_window
.set_title(_('Playlist'))
263 self
.playlist_window
.set_transient_for(self
.main_window
)
264 self
.playlist_window
.add(self
.__playlist
_tab
)
266 self
.create_actions()
269 if platform
.FREMANTLE
:
270 window
.set_app_menu(self
.create_app_menu())
272 window
.set_menu(self
.create_menu())
273 window
.add(self
.__player
_tab
)
275 menu_vbox
= gtk
.VBox()
276 menu_vbox
.set_spacing(0)
277 window
.add(menu_vbox
)
278 menu_bar
= gtk
.MenuBar()
279 self
.create_desktop_menu(menu_bar
)
280 menu_vbox
.pack_start(menu_bar
, False, False, 0)
282 menu_vbox
.pack_end(self
.__player
_tab
, True, True, 6)
284 # Tie it all together!
285 self
.__ignore
_queue
_check
= False
286 self
.__window
_fullscreen
= False
288 if platform
.MAEMO
and interface
.headset_device
is not None:
289 # Enable play/pause with headset button
290 interface
.headset_device
.connect_to_signal(
291 'Condition', self
.handle_headset_button
)
293 self
.main_window
.connect('key-press-event', self
.on_key_press
)
294 player
.playlist
.register( 'file_queued', self
.on_file_queued
)
296 player
.playlist
.register( 'playlist-to-be-overwritten',
298 self
.__player
_tab
.register( 'select-current-item-request',
299 self
.__select
_current
_item
)
301 self
.main_window
.show_all()
303 # this should be done when the gui is ready
304 player
.init(filepath
=filename
)
306 pos_int
, dur_int
= player
.get_position_duration()
307 # This prevents bogus values from being set while seeking
308 if (pos_int
> 10**9) and (dur_int
> 10**9):
309 self
.set_progress_callback(pos_int
, dur_int
)
311 def create_actions(self
):
312 self
.action_open
= gtk
.Action('open', _('Open'), _('Open a file or playlist'), gtk
.STOCK_OPEN
)
313 self
.action_open
.connect('activate', self
.open_file_callback
)
314 self
.action_save
= gtk
.Action('save', _('Save playlist'), _('Save current playlist to file'), gtk
.STOCK_SAVE_AS
)
315 self
.action_save
.connect('activate', self
.save_to_playlist_callback
)
316 self
.action_playlist
= gtk
.Action('playlist', _('Playlist'), _('Open the current playlist'), None)
317 self
.action_playlist
.connect('activate', lambda a
: self
.playlist_window
.show())
318 self
.action_about
= gtk
.Action('about', _('About Panucci'), _('Show application version'), gtk
.STOCK_ABOUT
)
319 self
.action_about
.connect('activate', self
.about_callback
)
320 self
.action_quit
= gtk
.Action('quit', _('Quit'), _('Close Panucci'), gtk
.STOCK_QUIT
)
321 self
.action_quit
.connect('activate', self
.destroy
)
323 def create_desktop_menu(self
, menu_bar
):
324 file_menu_item
= gtk
.MenuItem(_('File'))
325 file_menu
= gtk
.Menu()
326 file_menu
.append(self
.action_open
.create_menu_item())
327 file_menu
.append(self
.action_save
.create_menu_item())
328 file_menu
.append(gtk
.SeparatorMenuItem())
329 file_menu
.append(self
.action_quit
.create_menu_item())
330 file_menu_item
.set_submenu(file_menu
)
331 menu_bar
.append(file_menu_item
)
333 tools_menu_item
= gtk
.MenuItem(_('Tools'))
334 tools_menu
= gtk
.Menu()
335 tools_menu
.append(self
.action_playlist
.create_menu_item())
336 tools_menu_item
.set_submenu(tools_menu
)
337 menu_bar
.append(tools_menu_item
)
339 help_menu_item
= gtk
.MenuItem(_('Help'))
340 help_menu
= gtk
.Menu()
341 help_menu
.append(self
.action_about
.create_menu_item())
342 help_menu_item
.set_submenu(help_menu
)
343 menu_bar
.append(help_menu_item
)
345 def create_app_menu(self
):
346 menu
= hildon
.AppMenu()
348 b
= gtk
.Button(_('Playlist'))
349 b
.connect('clicked', lambda b
: self
.__player
_tab
.notify('select-current-item-request'))
352 b
= gtk
.Button(_('About'))
353 b
.connect('clicked', self
.about_callback
)
359 def create_menu(self
):
363 menu_open
= gtk
.ImageMenuItem(_('Open playlist'))
365 gtk
.image_new_from_stock(gtk
.STOCK_OPEN
, gtk
.ICON_SIZE_MENU
))
366 menu_open
.connect("activate", self
.open_file_callback
)
367 menu
.append(menu_open
)
369 # the recent files menu
370 self
.menu_recent
= gtk
.MenuItem(_('Open recent playlist'))
371 menu
.append(self
.menu_recent
)
372 self
.create_recent_files_menu()
374 menu
.append(gtk
.SeparatorMenuItem())
376 menu_save
= gtk
.ImageMenuItem(_('Save current playlist'))
378 gtk
.image_new_from_stock(gtk
.STOCK_SAVE_AS
, gtk
.ICON_SIZE_MENU
))
379 menu_save
.connect("activate", self
.save_to_playlist_callback
)
380 menu
.append(menu_save
)
382 menu
.append(gtk
.SeparatorMenuItem())
384 # the settings sub-menu
385 menu_settings
= gtk
.MenuItem(_('Settings'))
386 menu
.append(menu_settings
)
388 menu_settings_sub
= gtk
.Menu()
389 menu_settings
.set_submenu(menu_settings_sub
)
391 menu_settings_enable_dual_action
= gtk
.CheckMenuItem(
392 _('Enable dual-action buttons') )
393 settings
.attach_checkbutton( menu_settings_enable_dual_action
,
394 'enable_dual_action_btn' )
395 menu_settings_sub
.append(menu_settings_enable_dual_action
)
397 menu_settings_lock_progress
= gtk
.CheckMenuItem(_('Lock Progress Bar'))
398 settings
.attach_checkbutton( menu_settings_lock_progress
,
400 menu_settings_sub
.append(menu_settings_lock_progress
)
402 menu_about
= gtk
.ImageMenuItem(gtk
.STOCK_ABOUT
)
403 menu_about
.connect("activate", self
.about_callback
)
404 menu
.append(menu_about
)
406 menu
.append(gtk
.SeparatorMenuItem())
408 menu_quit
= gtk
.ImageMenuItem(gtk
.STOCK_QUIT
)
409 menu_quit
.connect("activate", self
.destroy
)
410 menu
.append(menu_quit
)
414 def create_recent_files_menu( self
):
415 max_files
= settings
.max_recent_files
416 self
.recent_files
= player
.playlist
.get_recent_files(max_files
)
417 menu_recent_sub
= gtk
.Menu()
419 if len(self
.recent_files
) > 0:
420 for f
in self
.recent_files
:
421 # don't include the temporary playlist in the file list
422 if f
== panucci
.PLAYLIST_FILE
: continue
423 # don't include non-existant files
424 if not os
.path
.exists( f
): continue
425 filename
, extension
= os
.path
.splitext(os
.path
.basename(f
))
426 menu_item
= gtk
.MenuItem( filename
.replace('_', ' '))
427 menu_item
.connect('activate', self
.on_recent_file_activate
, f
)
428 menu_recent_sub
.append(menu_item
)
430 menu_item
= gtk
.MenuItem(_('No recent files available.'))
431 menu_item
.set_sensitive(False)
432 menu_recent_sub
.append(menu_item
)
434 self
.menu_recent
.set_submenu(menu_recent_sub
)
436 def notify(self
, message
):
437 """ Sends a notification using pynotify, returns message """
438 if platform
.DESKTOP
and have_pynotify
:
439 icon
= find_image('panucci_64x64.png')
440 notification
= pynotify
.Notification(self
.main_window
.get_title(), message
, icon
)
442 elif platform
.FREMANTLE
:
443 hildon
.hildon_banner_show_information(self
.main_window
, \
446 # Note: This won't work if we're not in the gtk main loop
447 markup
= '<b>%s</b>\n<small>%s</small>' % (self
.main_window
.get_title(), message
)
448 hildon
.hildon_banner_show_information_with_markup(self
.main_window
, None, markup
)
450 def destroy(self
, widget
):
455 def set_progress_indicator(self
, loading_title
=False):
456 if platform
.FREMANTLE
:
458 self
.main_window
.set_title(_('Loading...'))
459 hildon
.hildon_gtk_window_set_progress_indicator(self
.main_window
, \
461 while gtk
.events_pending():
462 gtk
.main_iteration(False)
464 def show_main_window(self
):
465 self
.main_window
.present()
467 def check_queue(self
):
468 """ Makes sure the queue is saved if it has been modified
469 True means a new file can be opened
470 False means the user does not want to continue """
472 if not self
.__ignore
_queue
_check
and player
.playlist
.queue_modified
:
474 self
.main_window
, _('Save current playlist'),
475 _('Current playlist has been modified'),
476 _('Opening a new file will replace the current playlist. ') +
477 _('Do you want to save it before creating a new one?'),
478 affirmative_button
=gtk
.STOCK_SAVE
,
479 negative_button
=_('Discard changes'))
481 self
.__log
.debug('Response to "Save Queue?": %s', response
)
486 return self
.save_to_playlist_callback()
494 def open_file_callback(self
, widget
=None):
495 if self
.check_queue():
496 # set __ingnore__queue_check because we already did the check
497 self
.__ignore
_queue
_check
= True
498 filename
= get_file_from_filechooser(self
.main_window
)
499 if filename
is not None:
500 self
._play
_file
(filename
)
502 self
.__ignore
_queue
_check
= False
504 def save_to_playlist_callback(self
, widget
=None):
505 filename
= get_file_from_filechooser(
506 self
.main_window
, save_file
=True, save_to
='playlist.m3u' )
511 if os
.path
.isfile(filename
):
512 response
= dialog( self
.main_window
, _('File already exists'),
513 _('File already exists'),
514 _('The file %s already exists. You can choose another name or '
515 'overwrite the existing file.') % os
.path
.basename(filename
),
516 affirmative_button
=gtk
.STOCK_SAVE
,
517 negative_button
=_('Rename file'))
525 return self
.save_to_playlist_callback()
527 ext
= util
.detect_filetype(filename
)
528 if not player
.playlist
.save_to_new_playlist(filename
, ext
):
529 self
.notify(_('Error saving playlist...'))
534 def __get_fullscreen(self
):
535 return self
.__window
_fullscreen
537 def __set_fullscreen(self
, value
):
538 if value
!= self
.__window
_fullscreen
:
540 self
.main_window
.fullscreen()
542 self
.main_window
.unfullscreen()
544 self
.__window
_fullscreen
= value
545 player
.playlist
.send_metadata()
547 fullscreen
= property( __get_fullscreen
, __set_fullscreen
)
549 def on_key_press(self
, widget
, event
):
551 if event
.keyval
== gtk
.keysyms
.F6
:
552 self
.fullscreen
= not self
.fullscreen
554 def on_recent_file_activate(self
, widget
, filepath
):
555 self
._play
_file
(filepath
)
557 def on_file_queued(self
, filepath
, success
, notify
):
559 filename
= os
.path
.basename(filepath
)
562 self
.notify( '%s added successfully.' % filename
))
565 self
.notify( 'Error adding %s to the queue.' % filename
))
567 def about_callback(self
, widget
):
568 if platform
.FREMANTLE
:
569 from panucci
.aboutdialog
import HeAboutDialog
571 HeAboutDialog
.present(self
.main_window
,
581 about
= gtk
.AboutDialog()
582 about
.set_transient_for(self
.main_window
)
583 about
.set_name(about_name
)
584 about
.set_version(panucci
.__version
__)
585 about
.set_copyright(about_copyright
)
586 about
.set_comments(about_text
)
587 about
.set_website(about_website
)
588 about
.set_authors(about_authors
)
589 about
.set_translator_credits(_('translator-credits'))
590 about
.set_logo_icon_name('panucci')
594 def _play_file(self
, filename
, pause_on_load
=False):
595 player
.playlist
.load( os
.path
.abspath(filename
) )
597 if player
.playlist
.is_empty
:
600 def handle_headset_button(self
, event
, button
):
601 if event
== 'ButtonPressed' and button
== 'phone':
602 player
.play_pause_toggle()
604 def __select_current_item( self
):
605 # Select the currently playing track in the playlist tab
606 # and switch to it (so we can edit bookmarks, etc.. there)
607 self
.__playlist
_tab
.select_current_item()
608 self
.playlist_window
.show()
610 ##################################################
612 ##################################################
613 class PlayerTab(ObservableService
, gtk
.HBox
):
614 """ The tab that holds the player elements """
616 signals
= [ 'select-current-item-request', ]
618 def __init__(self
, gui_root
):
619 self
.__log
= logging
.getLogger('panucci.panucci.PlayerTab')
620 self
.__gui
_root
= gui_root
622 gtk
.HBox
.__init
__(self
)
623 ObservableService
.__init
__(self
, self
.signals
, self
.__log
)
626 self
.progress_timer_id
= None
628 self
.recent_files
= []
629 self
.make_player_tab()
630 self
.has_coverart
= False
632 #settings.register( 'enable_dual_action_btn_changed',
633 # self.on_dual_action_setting_changed )
634 #settings.register( 'dual_action_button_delay_changed',
635 # self.on_dual_action_setting_changed )
636 #settings.register( 'scrolling_labels_changed', lambda v:
637 # setattr( self.title_label, 'scrolling', v ) )
639 player
.register( 'stopped', self
.on_player_stopped
)
640 player
.register( 'playing', self
.on_player_playing
)
641 player
.register( 'paused', self
.on_player_paused
)
642 player
.playlist
.register( 'end-of-playlist',
643 self
.on_player_end_of_playlist
)
644 player
.playlist
.register( 'new-track-loaded',
645 self
.on_player_new_track
)
646 player
.playlist
.register( 'new-metadata-available',
647 self
.on_player_new_metadata
)
649 def make_player_tab(self
):
650 main_vbox
= gtk
.VBox()
651 main_vbox
.set_spacing(6)
653 self
.pack_start(main_vbox
, True, True)
655 # a hbox to hold the cover art and metadata vbox
656 metadata_hbox
= gtk
.HBox()
657 metadata_hbox
.set_spacing(6)
658 main_vbox
.pack_start(metadata_hbox
, True, False)
660 self
.cover_art
= gtk
.Image()
661 metadata_hbox
.pack_start( self
.cover_art
, False, False )
663 # vbox to hold metadata
664 metadata_vbox
= gtk
.VBox()
665 metadata_vbox
.pack_start(gtk
.Image(), True, True)
666 self
.artist_label
= gtk
.Label('')
667 self
.artist_label
.set_ellipsize(pango
.ELLIPSIZE_END
)
668 metadata_vbox
.pack_start(self
.artist_label
, False, False)
669 self
.album_label
= gtk
.Label('')
670 if platform
.FREMANTLE
:
671 hildon
.hildon_helper_set_logical_font(self
.album_label
, 'SmallSystemFont')
672 hildon
.hildon_helper_set_logical_color(self
.album_label
, gtk
.RC_FG
, gtk
.STATE_NORMAL
, 'SecondaryTextColor')
674 self
.album_label
.modify_font(pango
.FontDescription('normal 8'))
675 self
.album_label
.set_ellipsize(pango
.ELLIPSIZE_END
)
676 metadata_vbox
.pack_start(self
.album_label
, False, False)
677 self
.title_label
= widgets
.ScrollingLabel('',
680 delay_btwn_scrolls
=5000,
682 self
.title_label
.scrolling
= settings
.scrolling_labels
683 metadata_vbox
.pack_start(self
.title_label
, False, False)
684 metadata_vbox
.pack_start(gtk
.Image(), True, True)
685 metadata_hbox
.pack_start( metadata_vbox
, True, True )
687 progress_eventbox
= gtk
.EventBox()
688 progress_eventbox
.set_events(gtk
.gdk
.BUTTON_PRESS_MASK
)
689 progress_eventbox
.connect(
690 'button-press-event', self
.on_progressbar_changed
)
691 self
.progress
= gtk
.ProgressBar()
692 # make the progress bar more "finger-friendly"
693 if platform
.FREMANTLE
:
694 self
.progress
.set_size_request(-1, 100)
696 self
.progress
.set_size_request(-1, 50)
697 progress_eventbox
.add(self
.progress
)
698 main_vbox
.pack_start( progress_eventbox
, False, False )
700 # make the button box
701 buttonbox
= gtk
.HBox()
703 # A wrapper to help create DualActionButtons with the right settings
704 def create_da(widget
, action
, widget2
=None, action2
=None):
705 if platform
.FREMANTLE
:
709 return widgets
.DualActionButton(widget
, action
, \
711 settings
.dual_action_button_delay
, \
712 settings
.enable_dual_action_btn
)
714 self
.rrewind_button
= create_da(
715 generate_image('media-skip-backward.png'),
716 lambda: self
.do_seek(-1*settings
.seek_long
),
717 generate_image(gtk
.STOCK_GOTO_FIRST
, True),
718 player
.playlist
.prev
)
719 buttonbox
.add(self
.rrewind_button
)
721 self
.rewind_button
= create_da(
722 generate_image('media-seek-backward.png'),
723 lambda: self
.do_seek(-1*settings
.seek_short
))
724 buttonbox
.add(self
.rewind_button
)
726 self
.play_pause_button
= gtk
.Button('')
727 image(self
.play_pause_button
, 'media-playback-start.png')
728 self
.play_pause_button
.connect( 'clicked',
729 self
.on_btn_play_pause_clicked
)
730 self
.play_pause_button
.set_sensitive(False)
731 buttonbox
.add(self
.play_pause_button
)
733 self
.forward_button
= create_da(
734 generate_image('media-seek-forward.png'),
735 lambda: self
.do_seek(settings
.seek_short
))
736 buttonbox
.add(self
.forward_button
)
738 self
.fforward_button
= create_da(
739 generate_image('media-skip-forward.png'),
740 lambda: self
.do_seek(settings
.seek_long
),
741 generate_image(gtk
.STOCK_GOTO_LAST
, True),
742 player
.playlist
.next
)
743 buttonbox
.add(self
.fforward_button
)
745 self
.bookmarks_button
= create_da(
746 generate_image('bookmark-new.png'),
747 player
.add_bookmark_at_current_position
,
748 generate_image(gtk
.STOCK_JUMP_TO
, True),
749 lambda *args
: self
.notify('select-current-item-request'))
750 buttonbox
.add(self
.bookmarks_button
)
751 self
.set_controls_sensitivity(False)
753 if platform
.FREMANTLE
:
754 for child
in buttonbox
.get_children():
755 if isinstance(child
, gtk
.Button
):
756 child
.set_name('HildonButton-thumb')
757 buttonbox
.set_size_request(-1, 105)
759 main_vbox
.pack_start(buttonbox
, False, False)
762 self
.__gui
_root
.main_window
.connect( 'key-press-event',
765 # Disable focus for all widgets, so we can use the cursor
766 # keys + enter to directly control our media player, which
767 # is handled by "key-press-event"
769 self
.rrewind_button
, self
.rewind_button
,
770 self
.play_pause_button
, self
.forward_button
,
771 self
.fforward_button
, self
.progress
,
772 self
.bookmarks_button
, ):
773 w
.unset_flags(gtk
.CAN_FOCUS
)
775 def set_controls_sensitivity(self
, sensitive
):
776 for button
in self
.forward_button
, self
.rewind_button
, \
777 self
.fforward_button
, self
.rrewind_button
:
779 button
.set_sensitive(sensitive
)
781 # the play/pause button should always be available except
782 # for when the player starts without a file
783 self
.play_pause_button
.set_sensitive(True)
785 def on_dual_action_setting_changed( self
, *args
):
786 for button
in self
.forward_button
, self
.rewind_button
, \
787 self
.fforward_button
, self
.rrewind_button
, \
788 self
.bookmarks_button
:
790 button
.set_longpress_enabled( settings
.enable_dual_action_btn
)
791 button
.set_duration( settings
.dual_action_button_delay
)
793 def on_key_press(self
, widget
, event
):
795 if event
.keyval
== gtk
.keysyms
.Left
: # seek back
796 self
.do_seek( -1 * settings
.seek_long
)
797 elif event
.keyval
== gtk
.keysyms
.Right
: # seek forward
798 self
.do_seek( settings
.seek_long
)
799 elif event
.keyval
== gtk
.keysyms
.Return
: # play/pause
800 self
.on_btn_play_pause_clicked()
802 def on_player_stopped(self
):
803 self
.stop_progress_timer()
804 self
.set_controls_sensitivity(False)
805 image(self
.play_pause_button
, 'media-playback-start.png')
807 def on_player_playing(self
):
808 self
.start_progress_timer()
809 image(self
.play_pause_button
, 'media-playback-pause.png')
810 self
.set_controls_sensitivity(True)
811 if platform
.FREMANTLE
:
812 hildon
.hildon_gtk_window_set_progress_indicator(\
813 self
.__gui
_root
.main_window
, False)
815 def on_player_new_track(self
):
816 for widget
in [self
.title_label
,self
.artist_label
,self
.album_label
]:
817 widget
.set_markup('')
820 self
.cover_art
.hide()
821 self
.has_coverart
= False
823 def on_player_new_metadata(self
):
824 metadata
= player
.playlist
.get_file_metadata()
825 self
.set_metadata(metadata
)
827 if not player
.playing
:
828 position
= player
.playlist
.get_current_position()
829 estimated_length
= metadata
.get('length', 0)
830 self
.set_progress_callback( position
, estimated_length
)
832 def on_player_paused( self
, position
, duration
):
833 self
.stop_progress_timer() # This should save some power
834 self
.set_progress_callback( position
, duration
)
835 image(self
.play_pause_button
, 'media-playback-start.png')
837 def on_player_end_of_playlist(self
, loop
):
840 def reset_progress(self
):
841 self
.progress
.set_fraction(0)
842 self
.set_progress_callback(0,0)
844 def set_progress_callback(self
, time_elapsed
, total_time
):
845 """ times must be in nanoseconds """
846 time_string
= "%s / %s" % ( util
.convert_ns(time_elapsed
),
847 util
.convert_ns(total_time
) )
848 self
.progress
.set_text( time_string
)
849 fraction
= float(time_elapsed
) / float(total_time
) if total_time
else 0
850 self
.progress
.set_fraction( fraction
)
852 def on_progressbar_changed(self
, widget
, event
):
853 if ( not settings
.progress_locked
and
854 event
.type == gtk
.gdk
.BUTTON_PRESS
and event
.button
== 1 ):
855 new_fraction
= event
.x
/float(widget
.get_allocation().width
)
856 resp
= player
.do_seek(percent
=new_fraction
)
858 # Preemptively update the progressbar to make seeking smoother
859 self
.set_progress_callback( *resp
)
861 def on_btn_play_pause_clicked(self
, widget
=None):
862 player
.play_pause_toggle()
864 def progress_timer_callback( self
):
865 if player
.playing
and not player
.seeking
:
866 pos_int
, dur_int
= player
.get_position_duration()
867 # This prevents bogus values from being set while seeking
868 if ( pos_int
> 10**9 ) and ( dur_int
> 10**9 ):
869 self
.set_progress_callback( pos_int
, dur_int
)
872 def start_progress_timer( self
):
873 if self
.progress_timer_id
is not None:
874 self
.stop_progress_timer()
876 self
.progress_timer_id
= gobject
.timeout_add(
877 1000, self
.progress_timer_callback
)
879 def stop_progress_timer( self
):
880 if self
.progress_timer_id
is not None:
881 gobject
.source_remove( self
.progress_timer_id
)
882 self
.progress_timer_id
= None
884 def get_coverart_size( self
):
886 if self
.__gui
_root
.fullscreen
:
887 size
= coverart_sizes
['maemo fullscreen']
889 size
= coverart_sizes
['maemo']
891 size
= coverart_sizes
['normal']
895 def set_coverart( self
, pixbuf
):
896 self
.cover_art
.set_from_pixbuf(pixbuf
)
897 self
.cover_art
.show()
898 self
.has_coverart
= True
900 def set_metadata( self
, tag_message
):
901 tags
= { 'title': self
.title_label
, 'artist': self
.artist_label
,
902 'album': self
.album_label
}
905 if tag_message
.has_key('image') and tag_message
['image'] is not None:
906 value
= tag_message
['image']
908 pbl
= gtk
.gdk
.PixbufLoader()
913 x
, y
= self
.get_coverart_size()
914 pixbuf
= pbl
.get_pixbuf()
915 pixbuf
= pixbuf
.scale_simple( x
, y
, gtk
.gdk
.INTERP_BILINEAR
)
916 self
.set_coverart(pixbuf
)
918 self
.__log
.exception('Error setting coverart...')
920 # set the text metadata
921 for tag
,value
in tag_message
.iteritems():
922 if tags
.has_key(tag
) and value
is not None and value
.strip():
924 tags
[tag
].set_markup('<big>'+cgi
.escape(value
)+'</big>')
926 self
.__log
.exception(str(e
))
927 tags
[tag
].set_alignment( 0.5*int(not self
.has_coverart
), 0.5)
931 # make the title bold
932 tags
[tag
].set_markup('<b><big>'+cgi
.escape(value
)+'</big></b>')
934 if not platform
.MAEMO
:
935 value
+= ' - Panucci'
937 if platform
.FREMANTLE
and len(value
) > 25:
938 value
= value
[:24] + '...'
940 self
.__gui
_root
.main_window
.set_title( value
)
942 def do_seek(self
, seek_amount
):
943 resp
= player
.do_seek(from_current
=seek_amount
*10**9)
945 # Preemptively update the progressbar to make seeking smoother
946 self
.set_progress_callback( *resp
)
950 ##################################################
952 ##################################################
953 class PlaylistTab(gtk
.VBox
):
954 def __init__(self
, main_window
):
955 gtk
.VBox
.__init
__(self
)
956 self
.__log
= logging
.getLogger('panucci.panucci.BookmarksWindow')
957 self
.main
= main_window
959 self
.__model
= gtk
.TreeStore(
960 # uid, name, position
961 gobject
.TYPE_STRING
, gobject
.TYPE_STRING
, gobject
.TYPE_STRING
)
964 self
.treeview
= gtk
.TreeView()
965 self
.treeview
.set_model(self
.__model
)
966 self
.treeview
.set_headers_visible(True)
967 tree_selection
= self
.treeview
.get_selection()
968 # This breaks drag and drop, only use single selection for now
969 # tree_selection.set_mode(gtk.SELECTION_MULTIPLE)
970 tree_selection
.connect('changed', self
.tree_selection_changed
)
972 # The tree lines look nasty on maemo
974 self
.treeview
.set_enable_tree_lines(True)
977 ncol
= gtk
.TreeViewColumn(_('Name'))
978 ncell
= gtk
.CellRendererText()
979 ncell
.set_property('ellipsize', pango
.ELLIPSIZE_END
)
980 ncell
.set_property('editable', True)
981 ncell
.connect('edited', self
.label_edited
)
982 ncol
.set_expand(True)
983 ncol
.pack_start(ncell
)
984 ncol
.add_attribute(ncell
, 'text', 1)
986 tcol
= gtk
.TreeViewColumn(_('Position'))
987 tcell
= gtk
.CellRendererText()
988 tcol
.pack_start(tcell
)
989 tcol
.add_attribute(tcell
, 'text', 2)
991 self
.treeview
.append_column(ncol
)
992 self
.treeview
.append_column(tcol
)
993 self
.treeview
.connect('drag-data-received', self
.drag_data_recieved
)
994 self
.treeview
.connect('drag_data_get', self
.drag_data_get_data
)
997 ( 'playlist_row_data', gtk
.TARGET_SAME_WIDGET
, 0 ) ]
999 self
.treeview
.enable_model_drag_source(
1000 gtk
.gdk
.BUTTON1_MASK
, treeview_targets
, gtk
.gdk
.ACTION_COPY
)
1002 self
.treeview
.enable_model_drag_dest(
1003 treeview_targets
, gtk
.gdk
.ACTION_COPY
)
1005 sw
= gtk
.ScrolledWindow()
1006 sw
.set_policy(gtk
.POLICY_AUTOMATIC
, gtk
.POLICY_AUTOMATIC
)
1007 sw
.set_shadow_type(gtk
.SHADOW_IN
)
1008 sw
.add(self
.treeview
)
1011 self
.hbox
= gtk
.HBox()
1013 self
.add_button
= gtk
.Button(gtk
.STOCK_NEW
)
1014 self
.add_button
.set_use_stock(True)
1015 set_stock_button_text( self
.add_button
, _('Add File') )
1016 self
.add_button
.connect('clicked', self
.add_file
)
1017 self
.hbox
.pack_start(self
.add_button
, True, True)
1019 self
.dir_button
= gtk
.Button(gtk
.STOCK_OPEN
)
1020 self
.dir_button
.set_use_stock(True)
1021 set_stock_button_text( self
.dir_button
, _('Add Directory') )
1022 self
.dir_button
.connect('clicked', self
.add_directory
)
1023 self
.hbox
.pack_start(self
.dir_button
, True, True)
1025 self
.remove_button
= gtk
.Button(gtk
.STOCK_REMOVE
)
1026 self
.remove_button
.set_use_stock(True)
1027 self
.remove_button
.connect('clicked', self
.remove_bookmark
)
1028 self
.hbox
.pack_start(self
.remove_button
, True, True)
1030 self
.jump_button
= gtk
.Button(gtk
.STOCK_JUMP_TO
)
1031 self
.jump_button
.set_use_stock(True)
1032 self
.jump_button
.connect('clicked', self
.jump_bookmark
)
1033 self
.hbox
.pack_start(self
.jump_button
, True, True)
1035 self
.info_button
= gtk
.Button()
1036 self
.info_button
.add(
1037 gtk
.image_new_from_stock(gtk
.STOCK_INFO
, gtk
.ICON_SIZE_BUTTON
))
1038 self
.info_button
.connect('clicked', self
.show_playlist_item_details
)
1039 self
.hbox
.pack_start(self
.info_button
, True, True)
1041 if platform
.FREMANTLE
:
1042 for child
in self
.hbox
.get_children():
1043 if isinstance(child
, gtk
.Button
):
1044 child
.set_name('HildonButton-thumb')
1045 self
.hbox
.set_size_request(-1, 105)
1047 self
.pack_start(self
.hbox
, False, True)
1049 player
.playlist
.register( 'file_queued',
1050 lambda x
,y
,z
: self
.update_model() )
1051 player
.playlist
.register( 'bookmark_added', self
.on_bookmark_added
)
1055 def tree_selection_changed(self
, treeselection
):
1056 count
= treeselection
.count_selected_rows()
1057 self
.remove_button
.set_sensitive(count
> 0)
1058 self
.jump_button
.set_sensitive(count
== 1)
1059 self
.info_button
.set_sensitive(count
== 1)
1061 def drag_data_get_data(
1062 self
, treeview
, context
, selection
, target_id
, timestamp
):
1064 treeselection
= treeview
.get_selection()
1065 model
, iter = treeselection
.get_selected()
1066 # only allow moving around top-level parents
1067 if model
.iter_parent(iter) is None:
1068 # send the path of the selected row
1069 data
= model
.get_string_from_iter(iter)
1070 selection
.set(selection
.target
, 8, data
)
1072 self
.__log
.debug("Can't move children...")
1074 def drag_data_recieved(
1075 self
, treeview
, context
, x
, y
, selection
, info
, timestamp
):
1077 drop_info
= treeview
.get_dest_row_at_pos(x
, y
)
1079 # TODO: If user drags the row past the last row, drop_info is None
1080 # I'm not sure if it's safe to simply assume that None is
1081 # euqivalent to the last row...
1082 if None not in [ drop_info
and selection
.data
]:
1083 model
= treeview
.get_model()
1084 path
, position
= drop_info
1086 from_iter
= model
.get_iter_from_string(selection
.data
)
1088 # make sure the to_iter doesn't have a parent
1089 to_iter
= model
.get_iter(path
)
1090 if model
.iter_parent(to_iter
) is not None:
1091 to_iter
= model
.iter_parent(to_iter
)
1093 from_row
= model
.get_path(from_iter
)[0]
1096 if ( position
== gtk
.TREE_VIEW_DROP_BEFORE
or
1097 position
== gtk
.TREE_VIEW_DROP_INTO_OR_BEFORE
):
1098 model
.move_before( from_iter
, to_iter
)
1099 to_row
= to_row
- 1 if from_row
< to_row
else to_row
1100 elif ( position
== gtk
.TREE_VIEW_DROP_AFTER
or
1101 position
== gtk
.TREE_VIEW_DROP_INTO_OR_AFTER
):
1102 model
.move_after( from_iter
, to_iter
)
1103 to_row
= to_row
+ 1 if from_row
> to_row
else to_row
1105 self
.__log
.debug('Drop not supported: %s', position
)
1107 # don't do anything if we're not actually moving rows around
1108 if from_row
!= to_row
:
1109 player
.playlist
.move_item( from_row
, to_row
)
1112 self
.__log
.debug('No drop_data or selection.data available')
1114 def update_model(self
):
1115 plist
= player
.playlist
1116 path_info
= self
.treeview
.get_path_at_pos(0,0)
1117 path
= path_info
[0] if path_info
is not None else None
1119 self
.__model
.clear()
1122 for item
, data
in plist
.get_playlist_item_ids():
1123 parent
= self
.__model
.append(None, (item
, data
.get('title'), None))
1125 for bid
, bname
, bpos
in plist
.get_bookmarks_from_item_id( item
):
1126 nice_bpos
= util
.convert_ns(bpos
)
1127 self
.__model
.append( parent
, (bid
, bname
, nice_bpos
) )
1129 self
.treeview
.expand_all()
1131 if path
is not None:
1132 self
.treeview
.scroll_to_cell(path
)
1134 def label_edited(self
, cellrenderer
, path
, new_text
):
1135 iter = self
.__model
.get_iter(path
)
1136 old_text
= self
.__model
.get_value(iter, 1)
1138 if new_text
.strip() and old_text
!= new_text
:
1139 # this loop will only run once, because only one cell can be
1140 # edited at a time, we use it to get the item and bookmark ids
1141 for m
, bkmk_id
, biter
, item_id
, iiter
in self
.__cur
_selection
():
1142 self
.__model
.set_value(iter, 1, new_text
)
1143 player
.playlist
.update_bookmark(
1144 item_id
, bkmk_id
, name
=new_text
)
1146 self
.__model
.set_value(iter, 1, old_text
)
1148 def on_bookmark_added(self
, parent_id
, bookmark_name
, position
):
1149 self
.main
.notify(_('Bookmark added: %s') % bookmark_name
)
1152 def add_file(self
, widget
):
1153 filename
= get_file_from_filechooser(self
.main
.main_window
)
1154 if filename
is not None:
1155 player
.playlist
.append(filename
)
1157 def add_directory(self
, widget
):
1158 directory
= get_file_from_filechooser(
1159 self
.main
.main_window
, folder
=True )
1160 if directory
is not None:
1161 player
.playlist
.load_directory(directory
, append
=True)
1163 def __cur_selection(self
):
1164 selection
= self
.treeview
.get_selection()
1165 model
, bookmark_paths
= selection
.get_selected_rows()
1167 # Convert the paths to gtk.TreeRowReference objects, because we
1168 # might modify the model while this generator is running
1169 bookmark_refs
= [gtk
.TreeRowReference(model
, p
) for p
in bookmark_paths
]
1171 for reference
in bookmark_refs
:
1172 bookmark_iter
= model
.get_iter(reference
.get_path())
1173 item_iter
= model
.iter_parent(bookmark_iter
)
1175 # bookmark_iter is actually an item_iter
1176 if item_iter
is None:
1177 item_iter
= bookmark_iter
1178 item_id
= model
.get_value(item_iter
, 0)
1179 bookmark_id
, bookmark_iter
= None, None
1181 bookmark_id
= model
.get_value(bookmark_iter
, 0)
1182 item_id
= model
.get_value(item_iter
, 0)
1184 yield model
, bookmark_id
, bookmark_iter
, item_id
, item_iter
1186 def remove_bookmark(self
, w
=None):
1187 for model
, bkmk_id
, bkmk_iter
, item_id
, item_iter
in self
.__cur
_selection
():
1188 player
.playlist
.remove_bookmark( item_id
, bkmk_id
)
1189 if bkmk_iter
is not None:
1190 model
.remove(bkmk_iter
)
1191 elif item_iter
is not None:
1192 model
.remove(item_iter
)
1194 def select_current_item(self
):
1195 model
= self
.treeview
.get_model()
1196 selection
= self
.treeview
.get_selection()
1197 current_item_id
= str(player
.playlist
.get_current_item())
1198 for row
in iter(model
):
1199 if model
.get_value(row
.iter, 0) == current_item_id
:
1200 selection
.unselect_all()
1201 self
.treeview
.set_cursor(row
.path
)
1202 self
.treeview
.scroll_to_cell(row
.path
, use_align
=True)
1205 def show_playlist_item_details(self
, w
):
1206 selection
= self
.treeview
.get_selection()
1207 if selection
.count_selected_rows() == 1:
1208 selected
= self
.__cur
_selection
().next()
1209 model
, bkmk_id
, bkmk_iter
, item_id
, item_iter
= selected
1210 playlist_item
= player
.playlist
.get_item_by_id(item_id
)
1211 PlaylistItemDetails(self
.main
, playlist_item
)
1213 def jump_bookmark(self
, w
):
1214 selected
= list(self
.__cur
_selection
())
1215 if len(selected
) == 1:
1216 # It should be guranteed by the fact that we only enable the
1217 # "Jump to" button when the selection count equals 1.
1218 model
, bkmk_id
, bkmk_iter
, item_id
, item_iter
= selected
.pop(0)
1219 player
.playlist
.load_from_bookmark_id(item_id
, bkmk_id
)
1221 # FIXME: The player/playlist should be able to take care of this
1222 if not player
.playing
:
1226 ##################################################
1227 # PlaylistItemDetails
1228 ##################################################
1229 class PlaylistItemDetails(gtk
.Dialog
):
1230 def __init__(self
, main
, playlist_item
):
1231 gtk
.Dialog
.__init
__(self
, _('Playlist item details'),
1232 main
.main_window
, gtk
.DIALOG_MODAL
)
1234 if not platform
.FREMANTLE
:
1235 self
.add_button(gtk
.STOCK_CLOSE
, gtk
.RESPONSE_OK
)
1238 self
.fill(playlist_item
)
1239 self
.set_has_separator(False)
1240 self
.set_resizable(False)
1245 def fill(self
, playlist_item
):
1246 t
= gtk
.Table(10, 2)
1247 self
.vbox
.pack_start(t
, expand
=False)
1249 metadata
= playlist_item
.metadata
1251 t
.attach(gtk
.Label(_('Custom title:')), 0, 1, 0, 1)
1252 t
.attach(gtk
.Label(_('ID:')), 0, 1, 1, 2)
1253 t
.attach(gtk
.Label(_('Playlist ID:')), 0, 1, 2, 3)
1254 t
.attach(gtk
.Label(_('Filepath:')), 0, 1, 3, 4)
1257 for key
in metadata
:
1258 if metadata
[key
] is not None:
1259 t
.attach( gtk
.Label(key
.capitalize()+':'),
1260 0, 1, row_num
, row_num
+1 )
1263 t
.foreach(lambda x
, y
: x
.set_alignment(1, 0.5), None)
1264 t
.foreach(lambda x
, y
: x
.set_markup('<b>%s</b>' % x
.get_label()), None)
1266 t
.attach(gtk
.Label(playlist_item
.title
or _('<not modified>')),1,2,0,1)
1267 t
.attach(gtk
.Label(str(playlist_item
)), 1, 2, 1, 2)
1268 t
.attach(gtk
.Label(playlist_item
.playlist_id
), 1, 2, 2, 3)
1269 t
.attach(gtk
.Label(playlist_item
.filepath
), 1, 2, 3, 4)
1272 for key
in metadata
:
1273 value
= metadata
[key
]
1275 value
= util
.convert_ns(value
)
1276 if metadata
[key
] is not None:
1277 t
.attach( gtk
.Label( str(value
) or _('<not set>')),
1278 1, 2, row_num
, row_num
+1)
1281 t
.foreach(lambda x
, y
: x
.get_alignment() == (0.5, 0.5) and \
1282 x
.set_alignment(0, 0.5), None)
1284 t
.set_border_width(8)
1285 t
.set_row_spacings(4)
1286 t
.set_col_spacings(8)
1288 l
= gtk
.ListStore(str, str)
1290 cr
= gtk
.CellRendererText()
1291 cr
.set_property('ellipsize', pango
.ELLIPSIZE_END
)
1292 c
= gtk
.TreeViewColumn(_('Title'), cr
, text
=0)
1295 c
= gtk
.TreeViewColumn(_('Time'), gtk
.CellRendererText(), text
=1)
1297 playlist_item
.load_bookmarks()
1298 for bookmark
in playlist_item
.bookmarks
:
1299 l
.append([bookmark
.bookmark_name
, \
1300 util
.convert_ns(bookmark
.seek_position
)])
1302 sw
= gtk
.ScrolledWindow()
1303 sw
.set_shadow_type(gtk
.SHADOW_IN
)
1305 sw
.set_policy(gtk
.POLICY_AUTOMATIC
, gtk
.POLICY_AUTOMATIC
)
1306 e
= gtk
.Expander(_('Bookmarks'))
1308 if not platform
.MAEMO
:
1309 self
.vbox
.pack_start(e
)
1312 def run(filename
=None):
1313 PanucciGUI(filename
)
1316 if __name__
== '__main__':
1317 log
.error( 'Use the "panucci" executable to run this program.' )
1318 log
.error( 'Exiting...' )