3 # This file is part of Panucci.
4 # Copyright (c) 2008-2010 The Panucci Audiobook and Podcast Player Project
6 # Panucci is free software: you can redistribute it and/or modify
7 # it under the terms of the GNU General Public License as published by
8 # the Free Software Foundation, either version 3 of the License, or
9 # (at your option) any later version.
11 # Panucci is distributed in the hope that it will be useful,
12 # but WITHOUT ANY WARRANTY; without even the implied warranty of
13 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14 # GNU General Public License for more details.
16 # You should have received a copy of the GNU General Public License
17 # along with Panucci. If not, see <http://www.gnu.org/licenses/>.
19 # Based on http://thpinfo.com/2008/panucci/:
20 # A resuming media player for Podcasts and Audiobooks
21 # Copyright (c) 2008-05-26 Thomas Perl <thpinfo.com>
22 # (based on http://pygstdocs.berlios.de/pygst-tutorial/seeking.html)
25 from __future__
import absolute_import
39 from panucci
import widgets
40 from panucci
import util
41 from panucci
import platform
43 log
= logging
.getLogger('panucci.panucci')
47 pynotify
.init('Panucci')
56 log
.critical( 'Using GTK widgets, install "python2.5-hildon" '
57 'for this to work properly.' )
59 from panucci
.settings
import settings
60 from panucci
.player
import player
61 from panucci
.dbusinterface
import interface
62 from panucci
.services
import ObservableService
64 about_name
= 'Panucci'
65 about_text
= _('Resuming audiobook and podcast player')
66 about_authors
= ['Thomas Perl', 'Nick (nikosapi)', 'Matthew Taylor']
67 about_website
= 'http://panucci.garage.maemo.org/'
72 'maemo fullscreen' : 275,
75 gtk
.icon_size_register('panucci-button', 32, 32)
77 def find_image(filename
):
78 locations
= ['./icons/', '../icons/', '/usr/share/panucci/',
79 os
.path
.dirname(sys
.argv
[0])+'/../icons/']
81 for location
in locations
:
82 if os
.path
.exists(location
+filename
):
83 return os
.path
.abspath(location
+filename
)
85 def generate_image(filename
, is_stock
=False):
88 image
= gtk
.image_new_from_stock(
89 filename
, gtk
.icon_size_from_name('panucci-button') )
91 filename
= find_image(filename
)
92 if filename
is not None:
93 image
= gtk
.image_new_from_file(filename
)
96 image
.set_padding(20, 20)
98 image
.set_padding(5, 5)
102 def image(widget
, filename
, is_stock
=False):
103 child
= widget
.get_child()
104 if child
is not None:
106 image
= generate_image(filename
, is_stock
)
107 if image
is not None:
110 def dialog( toplevel_window
, title
, question
, description
,
111 affirmative_button
=gtk
.STOCK_YES
, negative_button
=gtk
.STOCK_NO
,
112 abortion_button
=gtk
.STOCK_CANCEL
):
114 """Present the user with a yes/no/cancel dialog.
115 The return value is either True, False or None, depending on which
116 button has been pressed in the dialog:
118 affirmative button (default: Yes) => True
119 negative button (defaut: No) => False
120 abortion button (default: Cancel) => None
122 When the dialog is closed with the "X" button in the window manager
123 decoration, the return value is always None (same as abortion button).
125 You can set any of the affirmative_button, negative_button or
126 abortion_button values to "None" to hide the corresponding action.
128 dlg
= gtk
.MessageDialog( toplevel_window
, gtk
.DIALOG_MODAL
,
129 gtk
.MESSAGE_QUESTION
, message_format
=question
)
133 if abortion_button
is not None:
134 dlg
.add_button(abortion_button
, gtk
.RESPONSE_CANCEL
)
135 if negative_button
is not None:
136 dlg
.add_button(negative_button
, gtk
.RESPONSE_NO
)
137 if affirmative_button
is not None:
138 dlg
.add_button(affirmative_button
, gtk
.RESPONSE_YES
)
140 dlg
.format_secondary_text(description
)
145 if response
== gtk
.RESPONSE_YES
:
147 elif response
== gtk
.RESPONSE_NO
:
149 elif response
in [gtk
.RESPONSE_CANCEL
, gtk
.RESPONSE_DELETE_EVENT
]:
152 def get_file_from_filechooser(
153 toplevel_window
, folder
=False, save_file
=False, save_to
=None):
156 open_action
= gtk
.FILE_CHOOSER_ACTION_SELECT_FOLDER
158 open_action
= gtk
.FILE_CHOOSER_ACTION_OPEN
162 args
= ( toplevel_window
, gtk
.FILE_CHOOSER_ACTION_SAVE
)
164 args
= ( toplevel_window
, open_action
)
166 dlg
= hildon
.FileChooserDialog( *args
)
169 args
= ( _('Select file to save playlist to'), None,
170 gtk
.FILE_CHOOSER_ACTION_SAVE
,
171 (( gtk
.STOCK_CANCEL
, gtk
.RESPONSE_REJECT
,
172 gtk
.STOCK_SAVE
, gtk
.RESPONSE_OK
)) )
174 args
= ( _('Select podcast or audiobook'), None, open_action
,
175 (( gtk
.STOCK_CANCEL
, gtk
.RESPONSE_REJECT
,
176 gtk
.STOCK_OPEN
, gtk
.RESPONSE_OK
)) )
178 dlg
= gtk
.FileChooserDialog(*args
)
180 current_folder
= os
.path
.expanduser(settings
.last_folder
)
182 if current_folder
is not None and os
.path
.isdir(current_folder
):
183 dlg
.set_current_folder(current_folder
)
185 if save_file
and save_to
is not None:
186 dlg
.set_current_name(save_to
)
188 if dlg
.run() == gtk
.RESPONSE_OK
:
189 filename
= dlg
.get_filename()
190 settings
.last_folder
= dlg
.get_current_folder()
197 def set_stock_button_text( button
, text
):
198 alignment
= button
.get_child()
199 hbox
= alignment
.get_child()
200 image
, label
= hbox
.get_children()
203 ##################################################
205 ##################################################
206 class PanucciGUI(object):
207 """ The object that holds the entire panucci gui """
209 def __init__(self
, filename
=None):
210 self
.__log
= logging
.getLogger('panucci.panucci.PanucciGUI')
211 interface
.register_gui(self
)
213 # Build the base ui (window and menubar)
215 self
.app
= hildon
.Program()
216 window
= hildon
.Window()
217 self
.app
.add_window(window
)
219 window
= gtk
.Window(gtk
.WINDOW_TOPLEVEL
)
221 self
.main_window
= window
222 window
.set_title('Panucci')
223 self
.window_icon
= find_image('panucci.png')
224 if self
.window_icon
is not None:
225 window
.set_icon_from_file( self
.window_icon
)
226 window
.set_default_size(400, -1)
227 window
.set_border_width(0)
228 window
.connect("destroy", self
.destroy
)
230 # Add the tabs (they are private to prevent us from trying to do
231 # something like gui_root.player_tab.some_function() from inside
232 # playlist_tab or vice-versa)
233 self
.__player
_tab
= PlayerTab(self
)
234 self
.__playlist
_tab
= PlaylistTab(self
)
236 self
.playlist_window
= gtk
.Window(gtk
.WINDOW_TOPLEVEL
)
237 self
.playlist_window
.connect('delete-event', gtk
.Widget
.hide_on_delete
)
238 self
.playlist_window
.set_title(_('Panucci playlist'))
239 self
.playlist_window
.set_transient_for(self
.main_window
)
240 self
.playlist_window
.add(self
.__playlist
_tab
)
242 self
.create_actions()
245 window
.set_menu(self
.create_menu())
246 window
.add(self
.__player
_tab
)
248 menu_vbox
= gtk
.VBox()
249 menu_vbox
.set_spacing(0)
250 window
.add(menu_vbox
)
251 menu_bar
= gtk
.MenuBar()
252 self
.create_desktop_menu(menu_bar
)
253 menu_vbox
.pack_start(menu_bar
, False, False, 0)
255 menu_vbox
.pack_end(self
.__player
_tab
, True, True, 6)
257 # Tie it all together!
258 self
.__ignore
_queue
_check
= False
259 self
.__window
_fullscreen
= False
261 if platform
.MAEMO
and interface
.headset_device
is not None:
262 # Enable play/pause with headset button
263 interface
.headset_device
.connect_to_signal(
264 'Condition', self
.handle_headset_button
)
266 self
.main_window
.connect('key-press-event', self
.on_key_press
)
267 player
.playlist
.register( 'file_queued', self
.on_file_queued
)
269 player
.playlist
.register( 'playlist-to-be-overwritten',
271 self
.__player
_tab
.register( 'select-current-item-request',
272 self
.__select
_current
_item
)
274 self
.main_window
.show_all()
276 # this should be done when the gui is ready
277 player
.init(filepath
=filename
)
279 def create_actions(self
):
280 self
.action_open
= gtk
.Action('open', _('Open'), _('Open a file or playlist'), gtk
.STOCK_OPEN
)
281 self
.action_open
.connect('activate', self
.open_file_callback
)
282 self
.action_save
= gtk
.Action('save', _('Save playlist'), _('Save current playlist to file'), gtk
.STOCK_SAVE_AS
)
283 self
.action_save
.connect('activate', self
.save_to_playlist_callback
)
284 self
.action_playlist
= gtk
.Action('playlist', _('Playlist'), _('Open the current playlist'), None)
285 self
.action_playlist
.connect('activate', lambda a
: self
.playlist_window
.show())
286 self
.action_about
= gtk
.Action('about', _('About Panucci'), _('Show application version'), gtk
.STOCK_ABOUT
)
287 self
.action_about
.connect('activate', self
.about_callback
)
288 self
.action_quit
= gtk
.Action('quit', _('Quit'), _('Close Panucci'), gtk
.STOCK_QUIT
)
289 self
.action_quit
.connect('activate', self
.destroy
)
291 def create_desktop_menu(self
, menu_bar
):
292 file_menu_item
= gtk
.MenuItem(_('File'))
293 file_menu
= gtk
.Menu()
294 file_menu
.append(self
.action_open
.create_menu_item())
295 file_menu
.append(self
.action_save
.create_menu_item())
296 file_menu
.append(gtk
.SeparatorMenuItem())
297 file_menu
.append(self
.action_quit
.create_menu_item())
298 file_menu_item
.set_submenu(file_menu
)
299 menu_bar
.append(file_menu_item
)
301 tools_menu_item
= gtk
.MenuItem(_('Tools'))
302 tools_menu
= gtk
.Menu()
303 tools_menu
.append(self
.action_playlist
.create_menu_item())
304 tools_menu_item
.set_submenu(tools_menu
)
305 menu_bar
.append(tools_menu_item
)
307 help_menu_item
= gtk
.MenuItem(_('Help'))
308 help_menu
= gtk
.Menu()
309 help_menu
.append(self
.action_about
.create_menu_item())
310 help_menu_item
.set_submenu(help_menu
)
311 menu_bar
.append(help_menu_item
)
313 def create_menu(self
):
317 menu_open
= gtk
.ImageMenuItem(_('Open playlist'))
319 gtk
.image_new_from_stock(gtk
.STOCK_OPEN
, gtk
.ICON_SIZE_MENU
))
320 menu_open
.connect("activate", self
.open_file_callback
)
321 menu
.append(menu_open
)
323 # the recent files menu
324 self
.menu_recent
= gtk
.MenuItem(_('Open recent playlist'))
325 menu
.append(self
.menu_recent
)
326 self
.create_recent_files_menu()
328 menu
.append(gtk
.SeparatorMenuItem())
330 menu_save
= gtk
.ImageMenuItem(_('Save current playlist'))
332 gtk
.image_new_from_stock(gtk
.STOCK_SAVE_AS
, gtk
.ICON_SIZE_MENU
))
333 menu_save
.connect("activate", self
.save_to_playlist_callback
)
334 menu
.append(menu_save
)
336 menu
.append(gtk
.SeparatorMenuItem())
338 # the settings sub-menu
339 menu_settings
= gtk
.MenuItem(_('Settings'))
340 menu
.append(menu_settings
)
342 menu_settings_sub
= gtk
.Menu()
343 menu_settings
.set_submenu(menu_settings_sub
)
345 menu_settings_enable_dual_action
= gtk
.CheckMenuItem(
346 _('Enable dual-action buttons') )
347 settings
.attach_checkbutton( menu_settings_enable_dual_action
,
348 'enable_dual_action_btn' )
349 menu_settings_sub
.append(menu_settings_enable_dual_action
)
351 menu_settings_lock_progress
= gtk
.CheckMenuItem(_('Lock Progress Bar'))
352 settings
.attach_checkbutton( menu_settings_lock_progress
,
354 menu_settings_sub
.append(menu_settings_lock_progress
)
356 menu_about
= gtk
.ImageMenuItem(gtk
.STOCK_ABOUT
)
357 menu_about
.connect("activate", self
.about_callback
)
358 menu
.append(menu_about
)
360 menu
.append(gtk
.SeparatorMenuItem())
362 menu_quit
= gtk
.ImageMenuItem(gtk
.STOCK_QUIT
)
363 menu_quit
.connect("activate", self
.destroy
)
364 menu
.append(menu_quit
)
368 def create_recent_files_menu( self
):
369 max_files
= settings
.max_recent_files
370 self
.recent_files
= player
.playlist
.get_recent_files(max_files
)
371 menu_recent_sub
= gtk
.Menu()
373 if len(self
.recent_files
) > 0:
374 for f
in self
.recent_files
:
375 # don't include the temporary playlist in the file list
376 if f
== panucci
.PLAYLIST_FILE
: continue
377 # don't include non-existant files
378 if not os
.path
.exists( f
): continue
379 filename
, extension
= os
.path
.splitext(os
.path
.basename(f
))
380 menu_item
= gtk
.MenuItem( filename
.replace('_', ' '))
381 menu_item
.connect('activate', self
.on_recent_file_activate
, f
)
382 menu_recent_sub
.append(menu_item
)
384 menu_item
= gtk
.MenuItem(_('No recent files available.'))
385 menu_item
.set_sensitive(False)
386 menu_recent_sub
.append(menu_item
)
388 self
.menu_recent
.set_submenu(menu_recent_sub
)
390 def notify(self
, message
):
391 """ Sends a notification using pynotify, returns message """
392 if platform
.DESKTOP
and have_pynotify
:
393 icon
= find_image('panucci_64x64.png')
394 notification
= pynotify
.Notification(self
.main_window
.get_title(), message
, icon
)
397 # Note: This won't work if we're not in the gtk main loop
398 markup
= '<b>%s</b>\n<small>%s</small>' % (title
, message
)
399 hildon
.hildon_banner_show_information_with_markup(self
.main_window
, None, markup
)
401 def destroy(self
, widget
):
405 def show_main_window(self
):
406 self
.main_window
.present()
408 def check_queue(self
):
409 """ Makes sure the queue is saved if it has been modified
410 True means a new file can be opened
411 False means the user does not want to continue """
413 if not self
.__ignore
_queue
_check
and player
.playlist
.queue_modified
:
415 self
.main_window
, _('Save current playlist'),
416 _('Current playlist has been modified'),
417 _('Opening a new file will replace the current playlist. ') +
418 _('Do you want to save it before creating a new one?'),
419 affirmative_button
=gtk
.STOCK_SAVE
,
420 negative_button
=_('Discard changes'))
422 self
.__log
.debug('Response to "Save Queue?": %s', response
)
427 return self
.save_to_playlist_callback()
435 def open_file_callback(self
, widget
=None):
436 if self
.check_queue():
437 # set __ingnore__queue_check because we already did the check
438 self
.__ignore
_queue
_check
= True
439 filename
= get_file_from_filechooser(self
.main_window
)
440 if filename
is not None:
441 self
._play
_file
(filename
)
443 self
.__ignore
_queue
_check
= False
445 def save_to_playlist_callback(self
, widget
=None):
446 filename
= get_file_from_filechooser(
447 self
.main_window
, save_file
=True, save_to
='playlist.m3u' )
452 if os
.path
.isfile(filename
):
453 response
= dialog( self
.main_window
, _('File already exists'),
454 _('File already exists'),
455 _('The file %s already exists. You can choose another name or '
456 'overwrite the existing file.') % os
.path
.basename(filename
),
457 affirmative_button
=gtk
.STOCK_SAVE
,
458 negative_button
=_('Rename file'))
466 return self
.save_to_playlist_callback()
468 ext
= util
.detect_filetype(filename
)
469 if not player
.playlist
.save_to_new_playlist(filename
, ext
):
470 self
.notify(_('Error saving playlist...'))
475 def __get_fullscreen(self
):
476 return self
.__window
_fullscreen
478 def __set_fullscreen(self
, value
):
479 if value
!= self
.__window
_fullscreen
:
481 self
.main_window
.fullscreen()
483 self
.main_window
.unfullscreen()
485 self
.__window
_fullscreen
= value
486 player
.playlist
.send_metadata()
488 fullscreen
= property( __get_fullscreen
, __set_fullscreen
)
490 def on_key_press(self
, widget
, event
):
492 if event
.keyval
== gtk
.keysyms
.F6
:
493 self
.fullscreen
= not self
.fullscreen
495 def on_recent_file_activate(self
, widget
, filepath
):
496 self
._play
_file
(filepath
)
498 def on_file_queued(self
, filepath
, success
, notify
):
500 filename
= os
.path
.basename(filepath
)
503 self
.notify( '%s added successfully.' % filename
))
506 self
.notify( 'Error adding %s to the queue.' % filename
))
508 def about_callback(self
, widget
):
509 dialog
= gtk
.AboutDialog()
510 dialog
.set_transient_for(self
.main_window
)
511 dialog
.set_website(about_website
)
512 dialog
.set_website_label(about_website
)
513 dialog
.set_name(about_name
)
514 dialog
.set_authors(about_authors
)
515 dialog
.set_comments(about_text
)
516 dialog
.set_version(panucci
.__version
__)
520 def _play_file(self
, filename
, pause_on_load
=False):
521 player
.playlist
.load( os
.path
.abspath(filename
) )
523 if player
.playlist
.is_empty
:
526 def handle_headset_button(self
, event
, button
):
527 if event
== 'ButtonPressed' and button
== 'phone':
528 player
.play_pause_toggle()
530 def __select_current_item( self
):
531 # Select the currently playing track in the playlist tab
532 # and switch to it (so we can edit bookmarks, etc.. there)
533 self
.__playlist
_tab
.select_current_item()
534 self
.playlist_window
.show()
536 ##################################################
538 ##################################################
539 class PlayerTab(ObservableService
, gtk
.HBox
):
540 """ The tab that holds the player elements """
542 signals
= [ 'select-current-item-request', ]
544 def __init__(self
, gui_root
):
545 self
.__log
= logging
.getLogger('panucci.panucci.PlayerTab')
546 self
.__gui
_root
= gui_root
548 gtk
.HBox
.__init
__(self
)
549 ObservableService
.__init
__(self
, self
.signals
, self
.__log
)
552 self
.progress_timer_id
= None
553 self
.volume_timer_id
= None
555 self
.recent_files
= []
556 self
.make_player_tab()
557 self
.has_coverart
= False
558 self
.set_volume(settings
.volume
)
560 #settings.register( 'enable_dual_action_btn_changed',
561 # self.on_dual_action_setting_changed )
562 #settings.register( 'dual_action_button_delay_changed',
563 # self.on_dual_action_setting_changed )
564 #settings.register( 'volume_changed', self.set_volume )
565 #settings.register( 'scrolling_labels_changed', lambda v:
566 # setattr( self.title_label, 'scrolling', v ) )
568 player
.register( 'stopped', self
.on_player_stopped
)
569 player
.register( 'playing', self
.on_player_playing
)
570 player
.register( 'paused', self
.on_player_paused
)
571 player
.playlist
.register( 'end-of-playlist',
572 self
.on_player_end_of_playlist
)
573 player
.playlist
.register( 'new-track-loaded',
574 self
.on_player_new_track
)
575 player
.playlist
.register( 'new-metadata-available',
576 self
.on_player_new_metadata
)
578 def make_player_tab(self
):
579 main_vbox
= gtk
.VBox()
580 main_vbox
.set_spacing(6)
582 self
.pack_start(main_vbox
, True, True)
584 # a hbox to hold the cover art and metadata vbox
585 metadata_hbox
= gtk
.HBox()
586 metadata_hbox
.set_spacing(6)
587 main_vbox
.pack_start(metadata_hbox
, True, False)
589 self
.cover_art
= gtk
.Image()
590 metadata_hbox
.pack_start( self
.cover_art
, False, False )
592 # vbox to hold metadata
593 metadata_vbox
= gtk
.VBox()
594 metadata_vbox
.set_spacing(8)
595 empty_label
= gtk
.Label()
596 metadata_vbox
.pack_start(empty_label
, True, True)
597 self
.artist_label
= gtk
.Label('')
598 self
.artist_label
.set_ellipsize(pango
.ELLIPSIZE_END
)
599 metadata_vbox
.pack_start(self
.artist_label
, False, False)
600 self
.album_label
= gtk
.Label('')
601 self
.album_label
.set_ellipsize(pango
.ELLIPSIZE_END
)
602 metadata_vbox
.pack_start(self
.album_label
, False, False)
603 self
.title_label
= widgets
.ScrollingLabel('',
606 delay_btwn_scrolls
=5000,
608 self
.title_label
.scrolling
= settings
.scrolling_labels
609 metadata_vbox
.pack_start(self
.title_label
, False, False)
610 empty_label
= gtk
.Label()
611 metadata_vbox
.pack_start(empty_label
, True, True)
612 metadata_hbox
.pack_start( metadata_vbox
, True, True )
614 progress_eventbox
= gtk
.EventBox()
615 progress_eventbox
.set_events(gtk
.gdk
.BUTTON_PRESS_MASK
)
616 progress_eventbox
.connect(
617 'button-press-event', self
.on_progressbar_changed
)
618 self
.progress
= gtk
.ProgressBar()
619 # make the progress bar more "finger-friendly"
620 if platform
.FREMANTLE
:
621 self
.progress
.set_size_request(-1, 100)
623 self
.progress
.set_size_request(-1, 50)
624 progress_eventbox
.add(self
.progress
)
625 main_vbox
.pack_start( progress_eventbox
, False, False )
627 # make the button box
628 buttonbox
= gtk
.HBox()
630 # A wrapper to help create DualActionButtons with the right settings
631 create_da
= lambda a
, b
, c
=None, d
=None: widgets
.DualActionButton(
632 a
, b
, c
, d
, settings
.dual_action_button_delay
,
633 settings
.enable_dual_action_btn
)
635 self
.rrewind_button
= create_da(
636 generate_image('media-skip-backward.png'),
637 lambda: self
.do_seek(-1*settings
.seek_long
),
638 generate_image(gtk
.STOCK_GOTO_FIRST
, True),
639 player
.playlist
.prev
)
640 buttonbox
.add(self
.rrewind_button
)
642 self
.rewind_button
= create_da(
643 generate_image('media-seek-backward.png'),
644 lambda: self
.do_seek(-1*settings
.seek_short
))
645 buttonbox
.add(self
.rewind_button
)
647 self
.play_pause_button
= gtk
.Button('')
648 image(self
.play_pause_button
, 'media-playback-start.png')
649 self
.play_pause_button
.connect( 'clicked',
650 self
.on_btn_play_pause_clicked
)
651 self
.play_pause_button
.set_sensitive(False)
652 buttonbox
.add(self
.play_pause_button
)
654 self
.forward_button
= create_da(
655 generate_image('media-seek-forward.png'),
656 lambda: self
.do_seek(settings
.seek_short
))
657 buttonbox
.add(self
.forward_button
)
659 self
.fforward_button
= create_da(
660 generate_image('media-skip-forward.png'),
661 lambda: self
.do_seek(settings
.seek_long
),
662 generate_image(gtk
.STOCK_GOTO_LAST
, True),
663 player
.playlist
.next
)
664 buttonbox
.add(self
.fforward_button
)
666 self
.bookmarks_button
= create_da(
667 generate_image('bookmark-new.png'),
668 player
.add_bookmark_at_current_position
,
669 generate_image(gtk
.STOCK_JUMP_TO
, True),
670 lambda *args
: self
.notify('select-current-item-request'))
671 buttonbox
.add(self
.bookmarks_button
)
672 self
.set_controls_sensitivity(False)
673 main_vbox
.pack_start(buttonbox
, False, False)
676 self
.volume
= hildon
.VVolumebar()
677 self
.volume
.set_property('can-focus', False)
678 self
.volume
.connect('level_changed', self
.volume_changed_hildon
)
679 self
.volume
.connect('mute_toggled', self
.mute_toggled
)
680 self
.__gui
_root
.main_window
.connect( 'key-press-event',
682 if not platform
.FREMANTLE
:
683 self
.pack_start(self
.volume
, False, True)
685 # Add a button to pop out the volume bar
686 self
.volume_button
= gtk
.ToggleButton('')
687 image(self
.volume_button
, 'media-speaker.png')
688 self
.volume_button
.connect('clicked', self
.toggle_volumebar
)
690 'show', lambda x
: self
.volume_button
.set_active(True))
692 'hide', lambda x
: self
.volume_button
.set_active(False))
693 if not platform
.FREMANTLE
:
694 buttonbox
.add(self
.volume_button
)
695 self
.volume_button
.show()
697 # Disable focus for all widgets, so we can use the cursor
698 # keys + enter to directly control our media player, which
699 # is handled by "key-press-event"
701 self
.rrewind_button
, self
.rewind_button
,
702 self
.play_pause_button
, self
.forward_button
,
703 self
.fforward_button
, self
.progress
,
704 self
.bookmarks_button
, self
.volume_button
, ):
705 w
.unset_flags(gtk
.CAN_FOCUS
)
707 self
.volume
= gtk
.VolumeButton()
708 self
.volume
.connect('value-changed', self
.volume_changed_gtk
)
709 buttonbox
.add(self
.volume
)
712 self
.set_volume(settings
.volume
)
714 def set_controls_sensitivity(self
, sensitive
):
715 for button
in self
.forward_button
, self
.rewind_button
, \
716 self
.fforward_button
, self
.rrewind_button
:
718 button
.set_sensitive(sensitive
)
720 # the play/pause button should always be available except
721 # for when the player starts without a file
722 self
.play_pause_button
.set_sensitive(True)
724 def on_dual_action_setting_changed( self
, *args
):
725 for button
in self
.forward_button
, self
.rewind_button
, \
726 self
.fforward_button
, self
.rrewind_button
, \
727 self
.bookmarks_button
:
729 button
.set_longpress_enabled( settings
.enable_dual_action_btn
)
730 button
.set_duration( settings
.dual_action_button_delay
)
732 def on_key_press(self
, widget
, event
):
734 if event
.keyval
== gtk
.keysyms
.F7
: #plus
735 self
.set_volume( min( 1, self
.get_volume() + 0.10 ))
736 elif event
.keyval
== gtk
.keysyms
.F8
: #minus
737 self
.set_volume( max( 0, self
.get_volume() - 0.10 ))
738 elif event
.keyval
== gtk
.keysyms
.Left
: # seek back
739 self
.do_seek( -1 * settings
.seek_long
)
740 elif event
.keyval
== gtk
.keysyms
.Right
: # seek forward
741 self
.do_seek( settings
.seek_long
)
742 elif event
.keyval
== gtk
.keysyms
.Return
: # play/pause
743 self
.on_btn_play_pause_clicked()
745 # The following two functions get and set the
746 # volume from the volume control widgets.
747 def get_volume(self
):
749 return self
.volume
.get_level()/100.0
751 return self
.volume
.get_value()
753 def set_volume(self
, vol
):
754 """ vol is a float from 0 to 1 """
757 if platform
.FREMANTLE
:
758 # No volume setting on Maemo 5
762 self
.volume
.set_level(vol
*100.0)
764 self
.volume
.set_value(vol
)
766 def __set_volume_hide_timer(self
, timeout
, force_show
=False):
767 if force_show
or self
.volume_button
.get_active():
769 if self
.volume_timer_id
is not None:
770 gobject
.source_remove(self
.volume_timer_id
)
771 self
.volume_timer_id
= None
773 self
.volume_timer_id
= gobject
.timeout_add(
774 1000 * timeout
, self
.__volume
_hide
_callback
)
776 def __volume_hide_callback(self
):
777 self
.volume_timer_id
= None
781 def toggle_volumebar(self
, widget
=None):
782 if self
.volume_timer_id
is None:
783 self
.__set
_volume
_hide
_timer
(5)
785 self
.__volume
_hide
_callback
()
787 def volume_changed_gtk(self
, widget
, new_value
=0.5):
788 settings
.volume
= new_value
790 def volume_changed_hildon(self
, widget
):
791 self
.__set
_volume
_hide
_timer
( 4, force_show
=True )
792 settings
.volume
= widget
.get_level()/100.0
794 def mute_toggled(self
, widget
):
795 if widget
.get_mute():
798 settings
.volume
= widget
.get_level()/100.0
800 def on_player_stopped(self
):
801 self
.stop_progress_timer()
802 self
.set_controls_sensitivity(False)
803 image(self
.play_pause_button
, 'media-playback-start.png')
805 def on_player_playing(self
):
806 self
.start_progress_timer()
807 image(self
.play_pause_button
, 'media-playback-pause.png')
808 self
.set_controls_sensitivity(True)
810 def on_player_new_track(self
):
811 for widget
in [self
.title_label
,self
.artist_label
,self
.album_label
]:
812 widget
.set_markup('')
815 self
.cover_art
.hide()
816 self
.has_coverart
= False
818 def on_player_new_metadata(self
):
819 metadata
= player
.playlist
.get_file_metadata()
820 self
.set_metadata(metadata
)
822 if not player
.playing
:
823 position
= player
.playlist
.get_current_position()
824 estimated_length
= metadata
.get('length', 0)
825 self
.set_progress_callback( position
, estimated_length
)
827 def on_player_paused( self
, position
, duration
):
828 self
.stop_progress_timer() # This should save some power
829 self
.set_progress_callback( position
, duration
)
830 image(self
.play_pause_button
, 'media-playback-start.png')
832 def on_player_end_of_playlist(self
, loop
):
835 def reset_progress(self
):
836 self
.progress
.set_fraction(0)
837 self
.set_progress_callback(0,0)
839 def set_progress_callback(self
, time_elapsed
, total_time
):
840 """ times must be in nanoseconds """
841 time_string
= "%s / %s" % ( util
.convert_ns(time_elapsed
),
842 util
.convert_ns(total_time
) )
843 self
.progress
.set_text( time_string
)
844 fraction
= float(time_elapsed
) / float(total_time
) if total_time
else 0
845 self
.progress
.set_fraction( fraction
)
847 def on_progressbar_changed(self
, widget
, event
):
848 if ( not settings
.progress_locked
and
849 event
.type == gtk
.gdk
.BUTTON_PRESS
and event
.button
== 1 ):
850 new_fraction
= event
.x
/float(widget
.get_allocation().width
)
851 resp
= player
.do_seek(percent
=new_fraction
)
853 # Preemptively update the progressbar to make seeking smoother
854 self
.set_progress_callback( *resp
)
856 def on_btn_play_pause_clicked(self
, widget
=None):
857 player
.play_pause_toggle()
859 def progress_timer_callback( self
):
860 if player
.playing
and not player
.seeking
:
861 pos_int
, dur_int
= player
.get_position_duration()
862 # This prevents bogus values from being set while seeking
863 if ( pos_int
> 10**9 ) and ( dur_int
> 10**9 ):
864 self
.set_progress_callback( pos_int
, dur_int
)
867 def start_progress_timer( self
):
868 if self
.progress_timer_id
is not None:
869 self
.stop_progress_timer()
871 self
.progress_timer_id
= gobject
.timeout_add(
872 1000, self
.progress_timer_callback
)
874 def stop_progress_timer( self
):
875 if self
.progress_timer_id
is not None:
876 gobject
.source_remove( self
.progress_timer_id
)
877 self
.progress_timer_id
= None
879 def get_coverart_size( self
):
881 if self
.__gui
_root
.fullscreen
:
882 size
= coverart_sizes
['maemo fullscreen']
884 size
= coverart_sizes
['maemo']
886 size
= coverart_sizes
['normal']
890 def set_coverart( self
, pixbuf
):
891 self
.cover_art
.set_from_pixbuf(pixbuf
)
892 self
.cover_art
.show()
893 self
.has_coverart
= True
895 def set_metadata( self
, tag_message
):
896 tags
= { 'title': self
.title_label
, 'artist': self
.artist_label
,
897 'album': self
.album_label
}
900 if tag_message
.has_key('image') and tag_message
['image'] is not None:
901 value
= tag_message
['image']
903 pbl
= gtk
.gdk
.PixbufLoader()
908 x
, y
= self
.get_coverart_size()
909 pixbuf
= pbl
.get_pixbuf()
910 pixbuf
= pixbuf
.scale_simple( x
, y
, gtk
.gdk
.INTERP_BILINEAR
)
911 self
.set_coverart(pixbuf
)
913 self
.__log
.exception('Error setting coverart...')
915 # set the text metadata
916 for tag
,value
in tag_message
.iteritems():
917 if tags
.has_key(tag
) and value
is not None and value
.strip():
918 tags
[tag
].set_markup('<big>'+value
+'</big>')
919 tags
[tag
].set_alignment( 0.5*int(not self
.has_coverart
), 0.5)
923 # make the title bold
924 tags
[tag
].set_markup('<b><big>'+value
+'</big></b>')
926 if not platform
.MAEMO
:
927 value
+= ' - Panucci'
929 self
.__gui
_root
.main_window
.set_title( value
)
931 def do_seek(self
, seek_amount
):
932 resp
= player
.do_seek(from_current
=seek_amount
*10**9)
934 # Preemptively update the progressbar to make seeking smoother
935 self
.set_progress_callback( *resp
)
939 ##################################################
941 ##################################################
942 class PlaylistTab(gtk
.VBox
):
943 def __init__(self
, main_window
):
944 gtk
.VBox
.__init
__(self
)
945 self
.__log
= logging
.getLogger('panucci.panucci.BookmarksWindow')
946 self
.main
= main_window
948 self
.__model
= gtk
.TreeStore(
949 # uid, name, position
950 gobject
.TYPE_STRING
, gobject
.TYPE_STRING
, gobject
.TYPE_STRING
)
953 self
.treeview
= gtk
.TreeView()
954 self
.treeview
.set_model(self
.__model
)
955 self
.treeview
.set_headers_visible(True)
956 tree_selection
= self
.treeview
.get_selection()
957 # This breaks drag and drop, only use single selection for now
958 # tree_selection.set_mode(gtk.SELECTION_MULTIPLE)
959 tree_selection
.connect('changed', self
.tree_selection_changed
)
961 # The tree lines look nasty on maemo
963 self
.treeview
.set_enable_tree_lines(True)
966 ncol
= gtk
.TreeViewColumn(_('Name'))
967 ncell
= gtk
.CellRendererText()
968 ncell
.set_property('ellipsize', pango
.ELLIPSIZE_END
)
969 ncell
.set_property('editable', True)
970 ncell
.connect('edited', self
.label_edited
)
971 ncol
.set_expand(True)
972 ncol
.pack_start(ncell
)
973 ncol
.add_attribute(ncell
, 'text', 1)
975 tcol
= gtk
.TreeViewColumn(_('Position'))
976 tcell
= gtk
.CellRendererText()
977 tcol
.pack_start(tcell
)
978 tcol
.add_attribute(tcell
, 'text', 2)
980 self
.treeview
.append_column(ncol
)
981 self
.treeview
.append_column(tcol
)
982 self
.treeview
.connect('drag-data-received', self
.drag_data_recieved
)
983 self
.treeview
.connect('drag_data_get', self
.drag_data_get_data
)
986 ( 'playlist_row_data', gtk
.TARGET_SAME_WIDGET
, 0 ) ]
988 self
.treeview
.enable_model_drag_source(
989 gtk
.gdk
.BUTTON1_MASK
, treeview_targets
, gtk
.gdk
.ACTION_COPY
)
991 self
.treeview
.enable_model_drag_dest(
992 treeview_targets
, gtk
.gdk
.ACTION_COPY
)
994 sw
= gtk
.ScrolledWindow()
995 sw
.set_policy(gtk
.POLICY_AUTOMATIC
, gtk
.POLICY_AUTOMATIC
)
996 sw
.set_shadow_type(gtk
.SHADOW_IN
)
997 sw
.add(self
.treeview
)
1000 self
.hbox
= gtk
.HBox()
1002 self
.add_button
= gtk
.Button(gtk
.STOCK_NEW
)
1003 self
.add_button
.set_use_stock(True)
1004 set_stock_button_text( self
.add_button
, _('Add File') )
1005 self
.add_button
.connect('clicked', self
.add_file
)
1006 self
.hbox
.pack_start(self
.add_button
, True, True)
1008 self
.dir_button
= gtk
.Button(gtk
.STOCK_OPEN
)
1009 self
.dir_button
.set_use_stock(True)
1010 set_stock_button_text( self
.dir_button
, _('Add Directory') )
1011 self
.dir_button
.connect('clicked', self
.add_directory
)
1012 self
.hbox
.pack_start(self
.dir_button
, True, True)
1014 self
.remove_button
= widgets
.DualActionButton(
1015 generate_image(gtk
.STOCK_REMOVE
, True),
1016 self
.remove_bookmark
,
1017 generate_image(gtk
.STOCK_CANCEL
, True),
1018 lambda *a
: player
.playlist
.reset_playlist() )
1019 #self.remove_button.set_use_stock(True)
1020 #self.remove_button.connect('clicked', self.remove_bookmark)
1021 self
.hbox
.pack_start(self
.remove_button
, True, True)
1023 self
.jump_button
= gtk
.Button(gtk
.STOCK_JUMP_TO
)
1024 self
.jump_button
.set_use_stock(True)
1025 self
.jump_button
.connect('clicked', self
.jump_bookmark
)
1026 self
.hbox
.pack_start(self
.jump_button
, True, True)
1028 self
.info_button
= gtk
.Button()
1029 self
.info_button
.add(
1030 gtk
.image_new_from_stock(gtk
.STOCK_INFO
, gtk
.ICON_SIZE_BUTTON
))
1031 self
.info_button
.connect('clicked', self
.show_playlist_item_details
)
1032 self
.hbox
.pack_start(self
.info_button
, True, True)
1034 self
.pack_start(self
.hbox
, False, True)
1036 player
.playlist
.register( 'file_queued',
1037 lambda x
,y
,z
: self
.update_model() )
1038 player
.playlist
.register( 'bookmark_added', self
.on_bookmark_added
)
1042 def tree_selection_changed(self
, treeselection
):
1043 count
= treeselection
.count_selected_rows()
1044 self
.remove_button
.set_sensitive(count
> 0)
1045 self
.jump_button
.set_sensitive(count
== 1)
1046 self
.info_button
.set_sensitive(count
== 1)
1048 def drag_data_get_data(
1049 self
, treeview
, context
, selection
, target_id
, timestamp
):
1051 treeselection
= treeview
.get_selection()
1052 model
, iter = treeselection
.get_selected()
1053 # only allow moving around top-level parents
1054 if model
.iter_parent(iter) is None:
1055 # send the path of the selected row
1056 data
= model
.get_string_from_iter(iter)
1057 selection
.set(selection
.target
, 8, data
)
1059 self
.__log
.debug("Can't move children...")
1061 def drag_data_recieved(
1062 self
, treeview
, context
, x
, y
, selection
, info
, timestamp
):
1064 drop_info
= treeview
.get_dest_row_at_pos(x
, y
)
1066 # TODO: If user drags the row past the last row, drop_info is None
1067 # I'm not sure if it's safe to simply assume that None is
1068 # euqivalent to the last row...
1069 if None not in [ drop_info
and selection
.data
]:
1070 model
= treeview
.get_model()
1071 path
, position
= drop_info
1073 from_iter
= model
.get_iter_from_string(selection
.data
)
1075 # make sure the to_iter doesn't have a parent
1076 to_iter
= model
.get_iter(path
)
1077 if model
.iter_parent(to_iter
) is not None:
1078 to_iter
= model
.iter_parent(to_iter
)
1080 from_row
= model
.get_path(from_iter
)[0]
1083 if ( position
== gtk
.TREE_VIEW_DROP_BEFORE
or
1084 position
== gtk
.TREE_VIEW_DROP_INTO_OR_BEFORE
):
1085 model
.move_before( from_iter
, to_iter
)
1086 to_row
= to_row
- 1 if from_row
< to_row
else to_row
1087 elif ( position
== gtk
.TREE_VIEW_DROP_AFTER
or
1088 position
== gtk
.TREE_VIEW_DROP_INTO_OR_AFTER
):
1089 model
.move_after( from_iter
, to_iter
)
1090 to_row
= to_row
+ 1 if from_row
> to_row
else to_row
1092 self
.__log
.debug('Drop not supported: %s', position
)
1094 # don't do anything if we're not actually moving rows around
1095 if from_row
!= to_row
:
1096 player
.playlist
.move_item( from_row
, to_row
)
1099 self
.__log
.debug('No drop_data or selection.data available')
1101 def update_model(self
):
1102 plist
= player
.playlist
1103 path_info
= self
.treeview
.get_path_at_pos(0,0)
1104 path
= path_info
[0] if path_info
is not None else None
1106 self
.__model
.clear()
1109 for item
, data
in plist
.get_playlist_item_ids():
1110 parent
= self
.__model
.append(None, (item
, data
.get('title'), None))
1112 for bid
, bname
, bpos
in plist
.get_bookmarks_from_item_id( item
):
1113 nice_bpos
= util
.convert_ns(bpos
)
1114 self
.__model
.append( parent
, (bid
, bname
, nice_bpos
) )
1116 self
.treeview
.expand_all()
1118 if path
is not None:
1119 self
.treeview
.scroll_to_cell(path
)
1121 def label_edited(self
, cellrenderer
, path
, new_text
):
1122 iter = self
.__model
.get_iter(path
)
1123 old_text
= self
.__model
.get_value(iter, 1)
1125 if new_text
.strip() and old_text
!= new_text
:
1126 # this loop will only run once, because only one cell can be
1127 # edited at a time, we use it to get the item and bookmark ids
1128 for m
, bkmk_id
, biter
, item_id
, iiter
in self
.__cur
_selection
():
1129 self
.__model
.set_value(iter, 1, new_text
)
1130 player
.playlist
.update_bookmark(
1131 item_id
, bkmk_id
, name
=new_text
)
1133 self
.__model
.set_value(iter, 1, old_text
)
1135 def on_bookmark_added(self
, parent_id
, bookmark_name
, position
):
1136 self
.main
.notify(_('Bookmark added: %s') % bookmark_name
)
1139 def add_file(self
, widget
):
1140 filename
= get_file_from_filechooser(self
.main
.main_window
)
1141 if filename
is not None:
1142 player
.playlist
.append(filename
)
1144 def add_directory(self
, widget
):
1145 directory
= get_file_from_filechooser(
1146 self
.main
.main_window
, folder
=True )
1147 if directory
is not None:
1148 player
.playlist
.load_directory(directory
, append
=True)
1150 def __cur_selection(self
):
1151 selection
= self
.treeview
.get_selection()
1152 model
, bookmark_paths
= selection
.get_selected_rows()
1154 # Convert the paths to gtk.TreeRowReference objects, because we
1155 # might modify the model while this generator is running
1156 bookmark_refs
= [gtk
.TreeRowReference(model
, p
) for p
in bookmark_paths
]
1158 for reference
in bookmark_refs
:
1159 bookmark_iter
= model
.get_iter(reference
.get_path())
1160 item_iter
= model
.iter_parent(bookmark_iter
)
1162 # bookmark_iter is actually an item_iter
1163 if item_iter
is None:
1164 item_iter
= bookmark_iter
1165 item_id
= model
.get_value(item_iter
, 0)
1166 bookmark_id
, bookmark_iter
= None, None
1168 bookmark_id
= model
.get_value(bookmark_iter
, 0)
1169 item_id
= model
.get_value(item_iter
, 0)
1171 yield model
, bookmark_id
, bookmark_iter
, item_id
, item_iter
1173 def remove_bookmark(self
, w
=None):
1174 for model
, bkmk_id
, bkmk_iter
, item_id
, item_iter
in self
.__cur
_selection
():
1175 player
.playlist
.remove_bookmark( item_id
, bkmk_id
)
1176 if bkmk_iter
is not None:
1177 model
.remove(bkmk_iter
)
1178 elif item_iter
is not None:
1179 model
.remove(item_iter
)
1181 def select_current_item(self
):
1182 model
= self
.treeview
.get_model()
1183 selection
= self
.treeview
.get_selection()
1184 current_item_id
= str(player
.playlist
.get_current_item())
1185 for row
in iter(model
):
1186 if model
.get_value(row
.iter, 0) == current_item_id
:
1187 selection
.unselect_all()
1188 self
.treeview
.set_cursor(row
.path
)
1189 self
.treeview
.scroll_to_cell(row
.path
, use_align
=True)
1192 def show_playlist_item_details(self
, w
):
1193 selection
= self
.treeview
.get_selection()
1194 if selection
.count_selected_rows() == 1:
1195 selected
= self
.__cur
_selection
().next()
1196 model
, bkmk_id
, bkmk_iter
, item_id
, item_iter
= selected
1197 playlist_item
= player
.playlist
.get_item_by_id(item_id
)
1198 PlaylistItemDetails(self
.main
, playlist_item
)
1200 def jump_bookmark(self
, w
):
1201 selected
= list(self
.__cur
_selection
())
1202 if len(selected
) == 1:
1203 # It should be guranteed by the fact that we only enable the
1204 # "Jump to" button when the selection count equals 1.
1205 model
, bkmk_id
, bkmk_iter
, item_id
, item_iter
= selected
.pop(0)
1206 player
.playlist
.load_from_bookmark_id(item_id
, bkmk_id
)
1208 # FIXME: The player/playlist should be able to take care of this
1209 if not player
.playing
:
1213 ##################################################
1214 # PlaylistItemDetails
1215 ##################################################
1216 class PlaylistItemDetails(gtk
.Dialog
):
1217 def __init__(self
, main
, playlist_item
):
1218 gtk
.Dialog
.__init
__( self
, _('Playlist item details'),
1219 main
.main_window
, gtk
.DIALOG_MODAL
,
1220 (gtk
.STOCK_CLOSE
, gtk
.RESPONSE_OK
))
1223 self
.fill(playlist_item
)
1224 self
.set_has_separator(False)
1225 self
.set_resizable(False)
1230 def fill(self
, playlist_item
):
1231 t
= gtk
.Table(10, 2)
1232 self
.vbox
.pack_start(t
, expand
=False)
1234 metadata
= playlist_item
.metadata
1236 t
.attach(gtk
.Label(_('Custom title:')), 0, 1, 0, 1)
1237 t
.attach(gtk
.Label(_('ID:')), 0, 1, 1, 2)
1238 t
.attach(gtk
.Label(_('Playlist ID:')), 0, 1, 2, 3)
1239 t
.attach(gtk
.Label(_('Filepath:')), 0, 1, 3, 4)
1242 for key
in metadata
:
1243 if metadata
[key
] is not None:
1244 t
.attach( gtk
.Label(key
.capitalize()+':'),
1245 0, 1, row_num
, row_num
+1 )
1248 t
.foreach(lambda x
, y
: x
.set_alignment(1, 0.5), None)
1249 t
.foreach(lambda x
, y
: x
.set_markup('<b>%s</b>' % x
.get_label()), None)
1251 t
.attach(gtk
.Label(playlist_item
.title
or _('<not modified>')),1,2,0,1)
1252 t
.attach(gtk
.Label(str(playlist_item
)), 1, 2, 1, 2)
1253 t
.attach(gtk
.Label(playlist_item
.playlist_id
), 1, 2, 2, 3)
1254 t
.attach(gtk
.Label(playlist_item
.filepath
), 1, 2, 3, 4)
1257 for key
in metadata
:
1258 value
= metadata
[key
]
1260 value
= util
.convert_ns(value
)
1261 if metadata
[key
] is not None:
1262 t
.attach( gtk
.Label( str(value
) or _('<not set>')),
1263 1, 2, row_num
, row_num
+1)
1266 t
.foreach(lambda x
, y
: x
.get_alignment() == (0.5, 0.5) and \
1267 x
.set_alignment(0, 0.5), None)
1269 t
.set_border_width(8)
1270 t
.set_row_spacings(4)
1271 t
.set_col_spacings(8)
1273 l
= gtk
.ListStore(str, str)
1275 cr
= gtk
.CellRendererText()
1276 cr
.set_property('ellipsize', pango
.ELLIPSIZE_END
)
1277 c
= gtk
.TreeViewColumn(_('Title'), cr
, text
=0)
1280 c
= gtk
.TreeViewColumn(_('Time'), gtk
.CellRendererText(), text
=1)
1282 playlist_item
.load_bookmarks()
1283 for bookmark
in playlist_item
.bookmarks
:
1284 l
.append([bookmark
.bookmark_name
, \
1285 util
.convert_ns(bookmark
.seek_position
)])
1287 sw
= gtk
.ScrolledWindow()
1288 sw
.set_shadow_type(gtk
.SHADOW_IN
)
1290 sw
.set_policy(gtk
.POLICY_AUTOMATIC
, gtk
.POLICY_AUTOMATIC
)
1291 e
= gtk
.Expander(_('Bookmarks'))
1293 self
.vbox
.pack_start(e
)
1296 def run(filename
=None):
1297 PanucciGUI( filename
)
1300 if __name__
== '__main__':
1301 log
.error( 'Use the "panucci" executable to run this program.' )
1302 log
.error( 'Exiting...' )