When sending via Bluetooth, always rename/copy the file
[gpodder.git] / src / gpodder / gui.py
blobb184fb811470e6439d30f6a3c3245b343a8e5a32
1 # -*- coding: utf-8 -*-
3 # gPodder - A media aggregator and podcast client
4 # Copyright (C) 2005-2007 Thomas Perl <thp at perli.net>
6 # gPodder 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 # gPodder 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 this program. If not, see <http://www.gnu.org/licenses/>.
20 import os
21 import gtk
22 import gtk.gdk
23 import gobject
24 import pango
25 import sys
26 import shutil
27 import webbrowser
28 import subprocess
30 from xml.sax import saxutils
32 from threading import Event
33 from threading import Thread
34 from string import strip
36 from gpodder import util
37 from gpodder import opml
38 from gpodder import services
39 from gpodder import sync
40 from gpodder import download
41 from gpodder import SimpleGladeApp
42 from gpodder import trayicon
44 from libpodcasts import podcastChannel
45 from libpodcasts import channels_to_model
46 from libpodcasts import load_channels
47 from libpodcasts import save_channels
49 from libgpodder import gPodderLib
50 from liblogger import log
52 from libplayers import UserAppsReader
54 from libtagupdate import tagging_supported
56 app_name = "gpodder"
57 app_version = "unknown" # will be set in main() call
58 app_authors = [ 'Thomas Perl <thp@perli.net>' ]
59 app_copyright = 'Copyright (c) 2005-2007 Thomas Perl'
60 app_website = 'http://gpodder.berlios.de/'
62 # these will be filled with pathnames in bin/gpodder
63 glade_dir = [ 'share', 'gpodder' ]
64 icon_dir = [ 'share', 'pixmaps', 'gpodder.png' ]
65 scalable_dir = [ 'share', 'icons', 'hicolor', 'scalable', 'apps', 'gpodder.svg' ]
68 class GladeWidget(SimpleGladeApp.SimpleGladeApp):
69 gpodder_main_window = None
71 def __init__( self, **kwargs):
72 path = os.path.join( glade_dir, '%s.glade' % app_name)
73 root = self.__class__.__name__
74 domain = app_name
76 SimpleGladeApp.SimpleGladeApp.__init__( self, path, root, domain, **kwargs)
78 if root == 'gPodder':
79 GladeWidget.gpodder_main_window = self.gPodder
80 else:
81 # If we have a child window, set it transient for our main window
82 getattr( self, root).set_transient_for( GladeWidget.gpodder_main_window)
84 if hasattr( self, 'center_on_widget'):
85 ( x, y ) = self.gpodder_main_window.get_position()
86 a = self.center_on_widget.allocation
87 ( x, y ) = ( x + a.x, y + a.y )
88 ( w, h ) = ( a.width, a.height )
89 ( pw, ph ) = getattr( self, root).get_size()
90 getattr( self, root).move( x + w/2 - pw/2, y + h/2 - ph/2)
91 else:
92 getattr( self, root).set_position( gtk.WIN_POS_CENTER_ON_PARENT)
94 def notification(self, message, title=None):
95 util.idle_add(self.show_message, message, title)
97 def show_message( self, message, title = None):
98 if hasattr(self, 'tray_icon') and hasattr(self, 'minimized') and self.tray_icon and self.minimized:
99 if title is None:
100 title = 'gPodder'
101 self.tray_icon.send_notification(message, title)
102 return
104 dlg = gtk.MessageDialog( GladeWidget.gpodder_main_window, gtk.DIALOG_MODAL, gtk.MESSAGE_INFO, gtk.BUTTONS_OK)
106 if title:
107 dlg.set_title(str(title))
108 dlg.set_markup( '<span weight="bold" size="larger">%s</span>\n\n%s' % ( title, message ))
109 else:
110 dlg.set_markup( '<span weight="bold" size="larger">%s</span>' % ( message ))
112 dlg.run()
113 dlg.destroy()
115 def show_confirmation( self, message, title = None):
116 dlg = gtk.MessageDialog( GladeWidget.gpodder_main_window, gtk.DIALOG_MODAL, gtk.MESSAGE_QUESTION, gtk.BUTTONS_YES_NO)
118 if title:
119 dlg.set_title( title)
120 dlg.set_markup( '<span weight="bold" size="larger">%s</span>\n\n%s' % ( title, message ))
121 else:
122 dlg.set_markup('<span weight="bold" size="larger">%s</span>' % message)
124 response = dlg.run()
125 dlg.destroy()
127 return response == gtk.RESPONSE_YES
129 def show_copy_dialog( self, src_filename, dst_filename = None, dst_directory = None, title = _('Select destination')):
130 if dst_filename is None:
131 dst_filename = src_filename
133 if dst_directory is None:
134 dst_directory = os.path.expanduser( '~')
136 ( base, extension ) = os.path.splitext( src_filename)
138 if not dst_filename.endswith( extension):
139 dst_filename += extension
141 dlg = gtk.FileChooserDialog( title = title, parent = GladeWidget.gpodder_main_window, action = gtk.FILE_CHOOSER_ACTION_SAVE)
142 dlg.set_do_overwrite_confirmation( True)
144 dlg.set_current_name( os.path.basename( dst_filename))
145 dlg.set_current_folder( dst_directory)
147 dlg.add_button( gtk.STOCK_CANCEL, gtk.RESPONSE_CANCEL)
148 dlg.add_button( gtk.STOCK_SAVE, gtk.RESPONSE_OK)
150 if dlg.run() == gtk.RESPONSE_OK:
151 dst_filename = dlg.get_filename()
152 if not dst_filename.endswith( extension):
153 dst_filename += extension
155 log( 'Copying %s => %s', src_filename, dst_filename, sender = self)
157 try:
158 shutil.copyfile( src_filename, dst_filename)
159 except:
160 log( 'Error copying file.', sender = self, traceback = True)
162 dlg.destroy()
166 class gPodder(GladeWidget):
167 def new(self):
168 self.uar = None
170 gl = gPodderLib()
172 self.minimized = False
173 self.gPodder.connect('window-state-event', self.window_state_event)
175 self.tray_icon = None
176 self.show_hide_tray_icon()
177 self.already_notified_new_episodes = []
179 self.episode_description_shown = gl.config.episode_list_descriptions
180 self.itemShowToolbar.set_active(gl.config.show_toolbar)
181 self.itemShowDescription.set_active(gl.config.episode_list_descriptions)
182 self.update_view_settings()
184 if self.tray_icon:
185 if gl.config.start_iconified:
186 self.iconify_main_window()
187 elif gl.config.minimize_to_tray:
188 self.tray_icon.set_visible(False)
190 gl.config.connect_gtk_window( self.gPodder)
191 gl.config.connect_gtk_paned( 'paned_position', self.channelPaned)
193 while gtk.events_pending():
194 gtk.main_iteration( False)
196 if app_version.rfind( "svn") != -1:
197 self.gPodder.set_title( 'gPodder %s' % app_version)
199 self.default_title = self.gPodder.get_title()
201 # cell renderers for channel tree
202 namecolumn = gtk.TreeViewColumn( _('Channel'))
204 iconcell = gtk.CellRendererPixbuf()
205 namecolumn.pack_start( iconcell, False)
206 namecolumn.add_attribute( iconcell, 'pixbuf', 5)
208 namecell = gtk.CellRendererText()
209 namecell.set_property('ellipsize', pango.ELLIPSIZE_END)
210 namecolumn.pack_start( namecell, True)
211 namecolumn.add_attribute( namecell, 'markup', 2)
212 namecolumn.add_attribute( namecell, 'weight', 4)
214 iconcell = gtk.CellRendererPixbuf()
215 namecolumn.pack_start( iconcell, False)
216 namecolumn.add_attribute( iconcell, 'pixbuf', 3)
218 self.treeChannels.append_column( namecolumn)
220 # enable alternating colors hint
221 self.treeAvailable.set_rules_hint( True)
222 self.treeChannels.set_rules_hint( True)
224 # connect to tooltip signals
225 try:
226 self.treeChannels.set_property('has-tooltip', True)
227 self.treeChannels.connect('query-tooltip', self.treeview_channels_query_tooltip)
228 except:
229 log('No tooltips for channel navigator (need at least PyGTK 2.12)', sender = self)
230 self.last_tooltip_channel = None
232 # Add our context menu to treeAvailable
233 self.treeAvailable.connect('button-press-event', self.treeview_button_pressed)
234 self.treeChannels.connect('button-press-event', self.treeview_channels_button_pressed)
236 iconcell = gtk.CellRendererPixbuf()
237 iconcolumn = gtk.TreeViewColumn( _("Status"), iconcell, pixbuf = 4)
239 namecell = gtk.CellRendererText()
240 namecell.set_property('ellipsize', pango.ELLIPSIZE_END)
241 namecolumn = gtk.TreeViewColumn(_("Episode"), namecell, markup=6)
242 namecolumn.set_sizing(gtk.TREE_VIEW_COLUMN_AUTOSIZE)
243 namecolumn.set_expand(True)
245 sizecell = gtk.CellRendererText()
246 sizecolumn = gtk.TreeViewColumn( _("Size"), sizecell, text=2)
248 releasecell = gtk.CellRendererText()
249 releasecolumn = gtk.TreeViewColumn( _("Released"), releasecell, text=5)
251 for itemcolumn in (iconcolumn, namecolumn, sizecolumn, releasecolumn):
252 itemcolumn.set_reorderable(True)
253 self.treeAvailable.append_column(itemcolumn)
255 # enable search in treeavailable
256 self.treeAvailable.set_search_equal_func( self.treeAvailable_search_equal)
258 # enable multiple selection support
259 self.treeAvailable.get_selection().set_mode( gtk.SELECTION_MULTIPLE)
260 self.treeDownloads.get_selection().set_mode( gtk.SELECTION_MULTIPLE)
262 # columns and renderers for "download progress" tab
263 episodecell = gtk.CellRendererText()
264 episodecolumn = gtk.TreeViewColumn( _("Episode"), episodecell, text=0)
266 speedcell = gtk.CellRendererText()
267 speedcolumn = gtk.TreeViewColumn( _("Speed"), speedcell, text=1)
269 progresscell = gtk.CellRendererProgress()
270 progresscolumn = gtk.TreeViewColumn( _("Progress"), progresscell, value=2)
272 for itemcolumn in ( episodecolumn, speedcolumn, progresscolumn ):
273 self.treeDownloads.append_column( itemcolumn)
275 services.download_status_manager.register( 'list-changed', self.download_status_updated)
276 services.download_status_manager.register( 'progress-changed', self.download_progress_updated)
278 self.treeDownloads.set_model( services.download_status_manager.tree_model)
280 #Add Drag and Drop Support
281 flags = gtk.DEST_DEFAULT_ALL
282 targets = [ ('text/plain', 0, 2), ('STRING', 0, 3), ('TEXT', 0, 4) ]
283 actions = gtk.gdk.ACTION_DEFAULT | gtk.gdk.ACTION_COPY
284 self.treeChannels.drag_dest_set( flags, targets, actions)
285 self.treeChannels.connect( 'drag_data_received', self.drag_data_received)
287 # Subscribed channels
288 self.active_channel = None
289 self.channels = load_channels( load_items = False, offline = True)
291 # load list of user applications for audio playback
292 self.user_apps_reader = UserAppsReader('audio')
293 Thread(target = self.user_apps_reader.read).start()
295 # load list of user applications for video playback
296 self.user_video_apps_reader = UserAppsReader('video')
297 Thread(target = self.user_video_apps_reader.read).start()
299 # Clean up old, orphaned download files
300 gl.clean_up_downloads( delete_partial = True)
302 # Set the "Device" menu item for the first time
303 self.update_item_device()
305 # Now, update the feed cache, when everything's in place
306 self.update_feed_cache(force_update=gl.config.update_on_startup)
308 # Start the auto-update procedure
309 self.auto_update_procedure(first_run=True)
311 # Delete old episodes if the user wishes to
312 if gl.config.auto_remove_old_episodes:
313 old_episodes = self.get_old_episodes()
314 if len(old_episodes) > 0:
315 self.delete_episode_list(old_episodes, confirm=False)
316 self.updateComboBox()
319 def treeview_channels_query_tooltip(self, treeview, x, y, keyboard_tooltip, tooltip):
320 # FIXME: Do not hardcode treeview header height
321 HEADER_HEIGHT = 25
322 (path, column, rx, ry) = treeview.get_path_at_pos( x, y-HEADER_HEIGHT) or (None,)*4
324 if path is not None:
325 model = treeview.get_model()
326 iter = model.get_iter(path)
327 url = model.get_value(iter, 0)
328 for channel in self.channels:
329 if channel.url == url:
330 if self.last_tooltip_channel is not None and self.last_tooltip_channel != channel:
331 self.last_tooltip_channel = None
332 return False
333 self.last_tooltip_channel = channel
334 tooltip.set_icon(channel.get_cover_pixbuf())
335 diskspace_str = _('Used disk space: %s') % util.format_filesize(channel.save_dir_size)
336 tooltip.set_markup( '<b>%s</b>\n<small><i>%s</i></small>\n%s\n\n<small>%s</small>' % (saxutils.escape(channel.title), saxutils.escape(channel.url), saxutils.escape(channel.description), diskspace_str))
337 return True
339 self.last_tooltip_channel = None
340 return False
342 def treeview_channels_button_pressed( self, treeview, event):
343 if event.button == 3:
344 ( x, y ) = ( int(event.x), int(event.y) )
345 ( path, column, rx, ry ) = treeview.get_path_at_pos( x, y) or (None,)*4
347 paths = []
349 # Did the user right-click into a selection?
350 selection = treeview.get_selection()
351 if selection.count_selected_rows() and path:
352 ( model, paths ) = selection.get_selected_rows()
353 if path not in paths:
354 # We have right-clicked, but not into the
355 # selection, assume we don't want to operate
356 # on the selection
357 paths = []
359 # No selection or right click not in selection:
360 # Select the single item where we clicked
361 if not len( paths) and path:
362 treeview.grab_focus()
363 treeview.set_cursor( path, column, 0)
365 ( model, paths ) = ( treeview.get_model(), [ path ] )
367 # We did not find a selection, and the user didn't
368 # click on an item to select -- don't show the menu
369 if not len( paths):
370 return True
372 menu = gtk.Menu()
374 channel_title = model.get_value( model.get_iter( paths[0]), 1)
376 item = gtk.ImageMenuItem( _('Open download folder'))
377 item.set_image( gtk.image_new_from_icon_name( 'folder-open', gtk.ICON_SIZE_MENU))
378 item.connect( 'activate', lambda x: gPodderLib().open_folder( self.active_channel.save_dir))
379 menu.append( item)
381 menu.append( gtk.SeparatorMenuItem())
383 item = gtk.ImageMenuItem('')
384 ( label, image ) = item.get_children()
385 label.set_text( _('Edit %s') % channel_title)
386 item.set_image( gtk.image_new_from_stock( gtk.STOCK_EDIT, gtk.ICON_SIZE_MENU))
387 item.connect( 'activate', self.on_itemEditChannel_activate)
388 menu.append( item)
390 item = gtk.ImageMenuItem( _('Remove %s') % ( channel_title, ))
391 item.set_image( gtk.image_new_from_stock( gtk.STOCK_DELETE, gtk.ICON_SIZE_MENU))
392 item.connect( 'activate', self.on_itemRemoveChannel_activate)
393 menu.append( item)
395 menu.show_all()
396 menu.popup( None, None, None, event.button, event.time)
398 return True
400 def save_episode_as_file( self, url, *args):
401 episode = self.active_channel.find_episode( url)
403 self.show_copy_dialog( src_filename = episode.local_filename(), dst_filename = episode.sync_filename())
405 def copy_episode_bluetooth(self, url, *args):
406 episode = self.active_channel.find_episode(url)
407 filename = episode.local_filename()
409 gl = gPodderLib()
410 if gl.config.bluetooth_ask_always:
411 device = None
412 else:
413 device = gl.config.bluetooth_device_address
415 destfile = os.path.join(gl.tempdir, episode.sync_filename())
416 (base, ext) = os.path.splitext(filename)
417 if not destfile.endswith(ext):
418 destfile += ext
420 if gl.config.bluetooth_use_converter:
421 title = _('Converting file')
422 message = _('Please wait while gPodder converts your media file for bluetooth file transfer.')
423 dlg = gtk.MessageDialog(self.gPodder, gtk.DIALOG_MODAL, gtk.MESSAGE_INFO, gtk.BUTTONS_NONE)
424 dlg.set_title(title)
425 dlg.set_markup('<span weight="bold" size="larger">%s</span>\n\n%s'%(title, message))
426 dlg.show_all()
427 else:
428 dlg = None
430 def convert_and_send_thread(filename, destfile, device, dialog, notify):
431 if gl.config.bluetooth_use_converter:
432 p = subprocess.Popen([gl.config.bluetooth_converter, filename, destfile], stdout=sys.stdout, stderr=sys.stderr)
433 result = p.wait()
434 if dialog is not None:
435 dialog.destroy()
436 else:
437 try:
438 shutil.copyfile(filename, destfile)
439 result = 0
440 except:
441 log('Cannot copy "%s" to "%s".', sender=self)
442 result = 1
444 if result == 0 or not os.path.exists(destfile):
445 util.bluetooth_send_file(destfile, device)
446 else:
447 notify(_('Error converting file.'), _('Bluetooth file transfer'))
448 util.delete_file(destfile)
450 Thread(target=convert_and_send_thread, args=[filename, destfile, device, dlg, self.notification]).start()
452 def treeview_button_pressed( self, treeview, event):
453 if event.button == 3:
454 ( x, y ) = ( int(event.x), int(event.y) )
455 ( path, column, rx, ry ) = treeview.get_path_at_pos( x, y) or (None,)*4
457 paths = []
459 # Did the user right-click into a selection?
460 selection = self.treeAvailable.get_selection()
461 if selection.count_selected_rows() and path:
462 ( model, paths ) = selection.get_selected_rows()
463 if path not in paths:
464 # We have right-clicked, but not into the
465 # selection, assume we don't want to operate
466 # on the selection
467 paths = []
469 # No selection or right click not in selection:
470 # Select the single item where we clicked
471 if not len( paths) and path:
472 treeview.grab_focus()
473 treeview.set_cursor( path, column, 0)
475 ( model, paths ) = ( treeview.get_model(), [ path ] )
477 # We did not find a selection, and the user didn't
478 # click on an item to select -- don't show the menu
479 if not len( paths):
480 return True
482 first_url = model.get_value( model.get_iter( paths[0]), 0)
484 menu = gtk.Menu()
485 gl = gPodderLib()
487 ( can_play, can_download, can_transfer, can_cancel ) = self.play_or_download()
489 if len(paths) == 1:
490 # Single item, add episode information menu item
491 episode_title = model.get_value( model.get_iter( paths[0]), 1)
492 episode_url = model.get_value( model.get_iter( paths[0]), 0)
493 if len(episode_title) > 30:
494 episode_title = episode_title[:27] + '...'
495 item = gtk.ImageMenuItem('')
496 ( label, image ) = item.get_children()
497 label.set_text( _('Episode information: %s') % episode_title)
498 item.set_image( gtk.image_new_from_stock( gtk.STOCK_INFO, gtk.ICON_SIZE_MENU))
499 item.connect( 'activate', lambda w: self.on_treeAvailable_row_activated( self.treeAvailable))
500 menu.append( item)
501 if can_play:
502 item = gtk.ImageMenuItem( _('Save %s to folder...') % episode_title)
503 item.set_image( gtk.image_new_from_stock( gtk.STOCK_SAVE_AS, gtk.ICON_SIZE_MENU))
504 item.connect( 'activate', lambda w: self.save_episode_as_file( episode_url))
505 menu.append( item)
506 if gl.config.bluetooth_enabled:
507 if gl.config.bluetooth_ask_always:
508 bt_device_name = _('bluetooth device')
509 else:
510 bt_device_name = _('%s via bluetooth')%gl.config.bluetooth_device_name
511 item = gtk.ImageMenuItem(_('Send to %s') % bt_device_name)
512 item.set_image(gtk.image_new_from_icon_name('stock_bluetooth', gtk.ICON_SIZE_MENU))
513 item.connect('activate', lambda w: self.copy_episode_bluetooth(episode_url))
514 menu.append( item)
515 menu.append( gtk.SeparatorMenuItem())
516 else:
517 episode_title = _('%d selected episodes') % len(paths)
519 if can_play:
520 item = gtk.ImageMenuItem( _('Play %s') % episode_title)
521 item.set_image( gtk.image_new_from_stock( gtk.STOCK_MEDIA_PLAY, gtk.ICON_SIZE_MENU))
522 item.connect( 'activate', lambda w: self.on_treeAvailable_row_activated( self.toolPlay))
523 menu.append( item)
525 is_locked = gPodderLib().history_is_locked(first_url)
526 if not is_locked:
527 item = gtk.ImageMenuItem(_('Remove %s') % episode_title)
528 item.set_image(gtk.image_new_from_stock(gtk.STOCK_DELETE, gtk.ICON_SIZE_MENU))
529 item.connect('activate', self.on_btnDownloadedDelete_clicked)
530 menu.append(item)
532 if can_download:
533 item = gtk.ImageMenuItem( _('Download %s') % episode_title)
534 item.set_image( gtk.image_new_from_stock( gtk.STOCK_GO_DOWN, gtk.ICON_SIZE_MENU))
535 item.connect( 'activate', lambda w: self.on_treeAvailable_row_activated( self.toolDownload))
536 menu.append( item)
538 menu.append( gtk.SeparatorMenuItem())
539 is_downloaded = gPodderLib().history_is_downloaded( first_url)
540 if is_downloaded:
541 item = gtk.ImageMenuItem( _('Mark %s as not downloaded') % episode_title)
542 item.set_image( gtk.image_new_from_stock( gtk.STOCK_UNDELETE, gtk.ICON_SIZE_MENU))
543 item.connect( 'activate', lambda w: self.on_item_toggle_downloaded_activate( w, False, False))
544 menu.append( item)
545 else:
546 item = gtk.ImageMenuItem( _('Mark %s as deleted') % episode_title)
547 item.set_image( gtk.image_new_from_stock( gtk.STOCK_DELETE, gtk.ICON_SIZE_MENU))
548 item.connect( 'activate', lambda w: self.on_item_toggle_downloaded_activate( w, False, True))
549 menu.append( item)
551 if can_transfer:
552 item = gtk.ImageMenuItem( _('Transfer %s to %s') % ( episode_title, gPodderLib().get_device_name() ))
553 item.set_image( gtk.image_new_from_stock( gtk.STOCK_NETWORK, gtk.ICON_SIZE_MENU))
554 item.connect( 'activate', lambda w: self.on_treeAvailable_row_activated( self.toolTransfer))
555 menu.append( item)
557 if can_play:
558 menu.append( gtk.SeparatorMenuItem())
559 is_played = gPodderLib().history_is_played( first_url)
560 if is_played:
561 item = gtk.ImageMenuItem( _('Mark %s as unplayed') % episode_title)
562 item.set_image( gtk.image_new_from_stock( gtk.STOCK_CANCEL, gtk.ICON_SIZE_MENU))
563 item.connect( 'activate', lambda w: self.on_item_toggle_played_activate( w, False, False))
564 menu.append( item)
565 else:
566 item = gtk.ImageMenuItem( _('Mark %s as played') % episode_title)
567 item.set_image( gtk.image_new_from_stock( gtk.STOCK_APPLY, gtk.ICON_SIZE_MENU))
568 item.connect( 'activate', lambda w: self.on_item_toggle_played_activate( w, False, True))
569 menu.append( item)
571 menu.append(gtk.SeparatorMenuItem())
572 is_locked = gPodderLib().history_is_locked(first_url)
573 if is_locked:
574 item = gtk.ImageMenuItem(_('Unlock %s') % episode_title)
575 item.set_image(gtk.image_new_from_stock(gtk.STOCK_DIALOG_AUTHENTICATION, gtk.ICON_SIZE_MENU))
576 item.connect('activate', self.on_item_toggle_lock_activate)
577 menu.append(item)
578 else:
579 item = gtk.ImageMenuItem(_('Lock %s') % episode_title)
580 item.set_image(gtk.image_new_from_stock(gtk.STOCK_DIALOG_AUTHENTICATION, gtk.ICON_SIZE_MENU))
581 item.connect('activate', self.on_item_toggle_lock_activate)
582 menu.append(item)
584 if can_cancel:
585 item = gtk.ImageMenuItem( _('_Cancel download'))
586 item.set_image( gtk.image_new_from_stock( gtk.STOCK_STOP, gtk.ICON_SIZE_MENU))
587 item.connect( 'activate', lambda w: self.on_treeDownloads_row_activated( self.toolCancel))
588 menu.append( item)
590 menu.show_all()
591 menu.popup( None, None, None, event.button, event.time)
593 return True
595 def download_progress_updated( self, count, percentage):
596 title = [ self.default_title ]
598 if count == 1:
599 title.append( _('downloading one file'))
600 elif count > 1:
601 title.append( _('downloading %d files') % count)
603 if len(title) == 2:
604 title[1] = ''.join( [ title[1], ' (%d%%)' % ( percentage, ) ])
606 self.gPodder.set_title( ' - '.join( title))
608 def playback_episode( self, current_channel, current_podcast):
609 ( success, application ) = gPodderLib().playback_episode( current_channel, current_podcast)
610 if not success:
611 self.show_message( _('The selected player application cannot be found. Please check your media player settings in the preferences dialog.'), _('Error opening player: %s') % ( saxutils.escape( application), ))
612 self.download_status_updated()
614 def treeAvailable_search_equal( self, model, column, key, iter, data = None):
615 if model is None:
616 return True
618 key = key.lower()
620 # columns, as defined in libpodcasts' get model method
621 # 1 = episode title, 7 = description
622 columns = (1, 7)
624 for column in columns:
625 value = model.get_value( iter, column).lower()
626 if value.find( key) != -1:
627 return False
629 return True
631 def play_or_download( self):
632 if self.wNotebook.get_current_page() > 0:
633 return
635 ( can_play, can_download, can_transfer, can_cancel ) = (False,)*4
637 selection = self.treeAvailable.get_selection()
638 if selection.count_selected_rows() > 0:
639 (model, paths) = selection.get_selected_rows()
641 for path in paths:
642 url = model.get_value( model.get_iter( path), 0)
643 local_filename = model.get_value( model.get_iter( path), 8)
645 if os.path.exists( local_filename):
646 can_play = True
647 else:
648 if services.download_status_manager.is_download_in_progress( url):
649 can_cancel = True
650 else:
651 can_download = True
653 if util.file_type_by_extension( util.file_extension_from_url( url)) == 'torrent':
654 can_download = can_download or gPodderLib().config.use_gnome_bittorrent
656 can_download = can_download and not can_cancel
657 can_play = can_play and not can_cancel and not can_download
658 can_transfer = can_play and gPodderLib().config.device_type != 'none'
660 self.toolPlay.set_sensitive( can_play)
661 self.toolDownload.set_sensitive( can_download)
662 self.toolTransfer.set_sensitive( can_transfer)
663 self.toolCancel.set_sensitive( can_cancel)
665 return ( can_play, can_download, can_transfer, can_cancel )
667 def download_status_updated( self):
668 count = services.download_status_manager.count()
669 if count:
670 self.labelDownloads.set_text( _('Downloads (%d)') % count)
671 else:
672 self.labelDownloads.set_text( _('Downloads'))
674 for channel in self.channels:
675 channel.update_model()
677 self.updateComboBox()
679 def updateComboBox( self):
680 ( model, iter ) = self.treeChannels.get_selection().get_selected()
682 if model and iter:
683 selected = model.get_path( iter)
684 else:
685 selected = (0,)
687 rect = self.treeChannels.get_visible_rect()
688 self.treeChannels.set_model(channels_to_model(self.channels))
689 self.treeChannels.scroll_to_point( rect.x, rect.y)
690 while gtk.events_pending():
691 gtk.main_iteration( False)
692 self.treeChannels.scroll_to_point( rect.x, rect.y)
694 try:
695 self.treeChannels.get_selection().select_path( selected)
696 except:
697 log( 'Cannot set selection on treeChannels', sender = self)
698 self.on_treeChannels_cursor_changed( self.treeChannels)
700 def updateTreeView( self):
701 gl = gPodderLib()
703 if self.channels:
704 self.treeAvailable.set_model( self.active_channel.tree_model)
705 self.treeAvailable.columns_autosize()
706 self.play_or_download()
707 else:
708 if self.treeAvailable.get_model():
709 self.treeAvailable.get_model().clear()
711 def drag_data_received(self, widget, context, x, y, sel, ttype, time):
712 result = sel.data
713 self.add_new_channel( result)
715 def add_new_channel( self, result = None, ask_download_new = True):
716 result = util.normalize_feed_url( result)
718 if result:
719 for old_channel in self.channels:
720 if old_channel.url == result:
721 self.show_message( _('You have already subscribed to this channel: %s') % ( saxutils.escape( old_channel.title), ), _('Already added'))
722 log( 'Channel already exists: %s', result)
723 # Select the existing channel in combo box
724 for i in range( len( self.channels)):
725 if self.channels[i] == old_channel:
726 self.treeChannels.get_selection().select_path( (i,))
727 return
728 log( 'Adding new channel: %s', result)
729 try:
730 channel = podcastChannel.get_by_url( url = result, force_update = True)
731 except:
732 log('Error in podcastChannel.get_by_url(%s)', result, sender=self)
733 channel = None
735 if channel:
736 self.channels.append( channel)
737 save_channels( self.channels)
738 # download changed channels
739 self.update_feed_cache(force_update=False)
741 (username, password) = util.username_password_from_url( result)
742 if username and self.show_confirmation( _('You have supplied <b>%s</b> as username and a password for this feed. Would you like to use the same authentication data for downloading episodes?') % ( saxutils.escape( username), ), _('Password authentication')):
743 channel.username = username
744 channel.password = password
745 log('Saving authentication data for episode downloads..', sender = self)
746 channel.save_settings()
748 # ask user to download some new episodes
749 self.treeChannels.get_selection().select_path( (len( self.channels)-1,))
750 self.active_channel = channel
751 self.updateTreeView()
752 if ask_download_new:
753 self.on_btnDownloadNewer_clicked( None)
754 else:
755 title = _('Error adding channel')
756 message = _('The channel could not be added. Please check the spelling of the URL or try again later.')
757 self.show_message( message, title)
758 else:
759 if result:
760 title = _('URL scheme not supported')
761 message = _('gPodder currently only supports URLs starting with <b>http://</b>, <b>feed://</b> or <b>ftp://</b>.')
762 self.show_message( message, title)
764 def update_feed_cache_callback(self, progressbar, position, count):
765 title = self.channels[position].title
766 progression = _('Updating %s (%d/%d)')%(title, position+1, count)
767 progressbar.set_text(progression)
768 if self.tray_icon:
769 self.tray_icon.set_status(self.tray_icon.STATUS_UPDATING_FEED_CACHE, progression)
771 if count > 0:
772 progressbar.set_fraction(float(position)/float(count))
774 def update_feed_cache_finish_callback(self, force_update=False, please_wait_dialog=None):
775 if please_wait_dialog is not None:
776 please_wait_dialog.destroy()
778 self.updateComboBox()
780 gl = gPodderLib()
782 if self.tray_icon:
783 self.tray_icon.set_status(None)
784 if self.minimized and force_update:
785 new_episodes = []
786 # look for new episodes to notify
787 for channel in self.channels:
788 for episode in channel.get_new_episodes():
789 if not episode.url in self.already_notified_new_episodes:
790 new_episodes.append(episode)
791 self.already_notified_new_episodes.append(episode.url)
792 # notify new episodes
793 if len(new_episodes) > 0:
794 if len(new_episodes) == 1:
795 title = _('gPodder has found %s') % (_('one new episode:'),)
796 else:
797 title = _('gPodder has found %s') % (_('%i new episodes:') % len(new_episodes))
798 message = self.tray_icon.format_episode_list(new_episodes)
800 #auto download new episodes
801 if gl.config.auto_download_when_minimized:
802 message += '\n<i>(%s...)</i>' % _('downloading')
803 self.download_episode_list(new_episodes)
804 self.tray_icon.send_notification(message, title)
805 return
807 # open the episodes selection dialog
808 if force_update:
809 self.on_itemDownloadAllNew_activate( self.gPodder)
811 def update_feed_cache_proc( self, force_update, callback_proc = None, callback_error = None, finish_proc = None):
812 self.channels = load_channels( force_update = force_update, callback_proc = callback_proc, callback_error = callback_error, offline = not force_update)
813 if finish_proc:
814 finish_proc()
816 def update_feed_cache(self, force_update=True):
817 if self.tray_icon:
818 self.tray_icon.set_status(self.tray_icon.STATUS_UPDATING_FEED_CACHE)
820 # skip dialog if the window is not active and tray icon is displayed
821 if self.minimized and self.tray_icon:
822 show_update_dialog = False
823 else:
824 show_update_dialog = True
826 please_wait = None
827 if show_update_dialog:
828 if force_update:
829 title = _('Downloading podcast feeds')
830 heading = _('Downloading feeds')
831 else:
832 title = _('Loading podcast feeds')
833 heading = _('Loading feeds')
834 body = _('Podcast feeds contain channel metadata and information about current episodes.')
836 please_wait = gtk.Dialog(title, self.gPodder, gtk.DIALOG_MODAL, (gtk.STOCK_CANCEL, gtk.RESPONSE_CANCEL))
837 please_wait.set_transient_for(self.gPodder)
838 please_wait.set_position(gtk.WIN_POS_CENTER_ON_PARENT)
839 please_wait.vbox.set_spacing(5)
840 please_wait.set_border_width(5)
841 please_wait.set_resizable(False)
843 label_heading = gtk.Label()
844 label_heading.set_alignment(0.0, 0.5)
845 label_heading.set_markup('<span weight="bold" size="larger">%s</span>'%heading)
847 label_body = gtk.Label(body)
848 label_body.set_alignment(0.0, 0.5)
849 label_body.set_line_wrap(True)
851 progressbar = gtk.ProgressBar()
852 progressbar.set_ellipsize(pango.ELLIPSIZE_MIDDLE)
854 # put it all together
855 for widget in (label_heading, label_body, progressbar):
856 please_wait.vbox.pack_start(widget)
857 please_wait.show_all()
859 # center the dialog on the gPodder main window
860 (x, y) = self.gPodder.get_position()
861 (w, h) = self.gPodder.get_size()
862 (pw, ph) = please_wait.get_size()
863 please_wait.move(int(x+w/2-pw/2), int(y+h/2-ph/2))
865 # hide separator line
866 please_wait.set_has_separator(False)
868 # let's get down to business..
869 if show_update_dialog:
870 callback_proc = lambda pos, count: util.idle_add(self.update_feed_cache_callback, progressbar, pos, count)
871 else:
872 callback_proc = None
873 finish_proc = lambda: util.idle_add(self.update_feed_cache_finish_callback, force_update, please_wait)
875 args = (force_update, callback_proc, self.notification, finish_proc)
877 thread = Thread( target = self.update_feed_cache_proc, args = args)
878 thread.start()
880 if please_wait is not None:
881 please_wait.run()
882 please_wait.destroy()
884 def download_podcast_by_url( self, url, want_message_dialog = True, widget = None):
885 if self.active_channel is None:
886 return
888 current_channel = self.active_channel
889 current_podcast = current_channel.find_episode( url)
890 filename = current_podcast.local_filename()
892 if widget:
893 if (widget.get_name() == 'itemPlaySelected' or widget.get_name() == 'toolPlay') and os.path.exists( filename):
894 # addDownloadedItem just to make sure the episode is marked correctly in localdb
895 current_channel.addDownloadedItem( current_podcast)
896 # open the file now
897 if current_podcast.file_type() != 'torrent':
898 self.playback_episode( current_channel, current_podcast)
899 return
901 if widget.get_name() == 'treeAvailable':
902 play_callback = lambda: self.playback_episode( current_channel, current_podcast)
903 download_callback = lambda: self.download_podcast_by_url( url, want_message_dialog, None)
904 gpe = gPodderEpisode( episode = current_podcast, channel = current_channel, download_callback = download_callback, play_callback = play_callback, center_on_widget = self.treeAvailable)
905 return
907 if not os.path.exists( filename) and not services.download_status_manager.is_download_in_progress( current_podcast.url):
908 download.DownloadThread( current_channel, current_podcast, self.notification).start()
909 else:
910 if want_message_dialog and os.path.exists( filename) and not current_podcast.file_type() == 'torrent':
911 title = _('Episode already downloaded')
912 message = _('You have already downloaded this episode. Click on the episode to play it.')
913 self.show_message( message, title)
914 elif want_message_dialog and not current_podcast.file_type() == 'torrent':
915 title = _('Download in progress')
916 message = _('You are currently downloading this episode. Please check the download status tab to check when the download is finished.')
917 self.show_message( message, title)
919 if os.path.exists( filename):
920 log( 'Episode has already been downloaded.')
921 current_channel.addDownloadedItem( current_podcast)
922 self.updateComboBox()
924 def on_gPodder_delete_event(self, widget, *args):
925 """Called when the GUI wants to close the window
926 Displays a confirmation dialog (and closes/hides gPodder)
929 gl = gPodderLib()
930 downloading = services.download_status_manager.has_items()
932 if not gl.config.on_quit_ask and gl.config.on_quit_systray and self.tray_icon:
933 self.iconify_main_window()
934 elif gl.config.on_quit_ask or downloading:
935 dialog = gtk.MessageDialog(self.gPodder, gtk.DIALOG_MODAL, gtk.MESSAGE_QUESTION, gtk.BUTTONS_NONE)
936 dialog.add_button(gtk.STOCK_CANCEL, gtk.RESPONSE_CANCEL)
937 if self.tray_icon:
938 dialog.add_button(_('Hide gPodder'), gtk.RESPONSE_YES)
939 dialog.add_button(gtk.STOCK_QUIT, gtk.RESPONSE_CLOSE)
941 title = _('Quit gPodder')
942 if downloading:
943 message = _('You are downloading episodes. If you close gPodder now, the downloads will be aborted.')
944 elif self.tray_icon:
945 message = _('If you hide gPodder, it will continue to run in the system tray notification area.')
946 else:
947 message = _('Do you really want to quit gPodder now?')
949 dialog.set_title(title)
950 dialog.set_markup('<span weight="bold" size="larger">%s</span>\n\n%s'%(title, message))
951 if not downloading:
952 cb_ask = gtk.CheckButton(_("Don't ask me again"))
953 dialog.vbox.pack_start(cb_ask)
954 cb_ask.show_all()
956 result = dialog.run()
957 dialog.destroy()
959 if result == gtk.RESPONSE_CLOSE:
960 if not downloading and cb_ask.get_active() == True:
961 gl.config.on_quit_ask = False
962 gl.config.on_quit_systray = False
963 self.close_gpodder()
964 elif result == gtk.RESPONSE_YES:
965 if not downloading and cb_ask.get_active() == True:
966 gl.config.on_quit_ask = False
967 gl.config.on_quit_systray = True
968 self.iconify_main_window()
969 else:
970 self.close_gpodder()
972 return True
974 def close_gpodder(self):
975 """ clean everything and exit properly
977 if self.channels:
978 if not save_channels(self.channels):
979 self.show_message(_('Please check your permissions and free disk space.'), _('Error saving channel list'))
981 services.download_status_manager.cancel_all()
983 self.gtk_main_quit()
984 sys.exit( 0)
986 def get_old_episodes(self):
987 episodes = []
988 for channel in self.channels:
989 for episode in channel.get_all_episodes():
990 if episode.is_downloaded() and episode.is_old() and not episode.is_locked() and episode.is_played():
991 episodes.append(episode)
992 return episodes
994 def for_each_selected_episode_url( self, callback):
995 ( model, paths ) = self.treeAvailable.get_selection().get_selected_rows()
996 for path in paths:
997 url = model.get_value( model.get_iter( path), 0)
998 try:
999 callback( url)
1000 except:
1001 log( 'Warning: Error in for_each_selected_episode_url for URL %s', url, sender = self)
1002 self.active_channel.update_model()
1003 self.updateComboBox()
1005 def delete_episode_list( self, episodes, confirm = True):
1006 if len(episodes) == 0:
1007 return
1009 if len(episodes) == 1:
1010 message = _('Do you really want to delete this episode?')
1011 else:
1012 message = _('Do you really want to delete %d episodes?') % len(episodes)
1014 if confirm and self.show_confirmation( message, _('Delete episodes')) == False:
1015 return
1017 for episode in episodes:
1018 log('Deleting episode: %s', episode.title, sender = self)
1019 episode.delete_from_disk()
1021 self.download_status_updated()
1023 def on_itemRemoveOldEpisodes_activate( self, widget):
1024 columns = (
1025 ('title', _('Episode')),
1026 ('channel_prop', _('Channel')),
1027 ('filesize_prop', _('Size')),
1028 ('pubdate_prop', _('Released')),
1029 ('played_prop', _('Status')),
1030 ('age_prop', _('Downloaded')),
1033 gl = gPodderLib()
1034 selection_buttons = {
1035 _('Select played'): lambda episode: episode.is_played(),
1036 _('Select older than %d days') % gl.config.episode_old_age: lambda episode: episode.is_old(),
1039 instructions = _('Select the episodes you want to delete from your hard disk.')
1041 episodes = []
1042 selected = []
1043 for channel in self.channels:
1044 for episode in channel:
1045 if episode.is_downloaded() and not episode.is_locked():
1046 episodes.append( episode)
1047 selected.append( episode.is_played())
1049 gPodderEpisodeSelector( title = _('Remove old episodes'), instructions = instructions, \
1050 episodes = episodes, selected = selected, columns = columns, \
1051 stock_ok_button = gtk.STOCK_DELETE, callback = self.delete_episode_list, \
1052 selection_buttons = selection_buttons)
1054 def on_item_toggle_downloaded_activate( self, widget, toggle = True, new_value = False):
1055 if toggle:
1056 callback = lambda url: gPodderLib().history_mark_downloaded( url, not gPodderLib().history_is_downloaded( url))
1057 else:
1058 callback = lambda url: gPodderLib().history_mark_downloaded( url, new_value)
1060 self.for_each_selected_episode_url( callback)
1062 def on_item_toggle_played_activate( self, widget, toggle = True, new_value = False):
1063 if toggle:
1064 callback = lambda url: gPodderLib().history_mark_played( url, not gPodderLib().history_is_played( url))
1065 else:
1066 callback = lambda url: gPodderLib().history_mark_played( url, new_value)
1068 self.for_each_selected_episode_url( callback)
1070 def on_item_toggle_lock_activate(self, widget, toggle=True, new_value=False):
1071 if toggle:
1072 callback = lambda url: gPodderLib().history_mark_locked(url, not gPodderLib().history_is_locked(url))
1073 else:
1074 callback = lambda url: gPodderLib().history_mark_locked(url, new_value)
1076 self.for_each_selected_episode_url(callback)
1078 def on_itemUpdate_activate(self, widget, *args):
1079 if self.channels:
1080 self.update_feed_cache()
1081 else:
1082 title = _('No channels available')
1083 message = _('You need to subscribe to some podcast feeds before you can start downloading podcasts. Use your favorite search engine to look for interesting podcasts.')
1084 self.show_message( message, title)
1086 def download_episode_list( self, episodes):
1087 for episode in episodes:
1088 log('Downloading episode: %s', episode.title, sender = self)
1089 filename = episode.local_filename()
1090 if not os.path.exists( filename) and not services.download_status_manager.is_download_in_progress( episode.url):
1091 download.DownloadThread( episode.channel, episode, self.notification).start()
1093 def new_episodes_show(self, episodes):
1094 columns = (
1095 ('title', _('Episode')),
1096 ('channel_prop', _('Channel')),
1097 ('filesize_prop', _('Size')),
1098 ('pubdate_prop', _('Released')),
1101 if len(episodes) > 0:
1102 instructions = _('Select the episodes you want to download now.')
1104 gPodderEpisodeSelector(title=_('New episodes available'), instructions=instructions, \
1105 episodes=episodes, columns=columns, selected_default=True, \
1106 callback=self.download_episode_list)
1107 else:
1108 title = _('No new episodes')
1109 message = _('There are no new episodes to download from your podcast subscriptions. Please check for new episodes later.')
1110 self.show_message(message, title)
1112 def on_itemDownloadAllNew_activate(self, widget, *args):
1113 episodes = []
1114 for channel in self.channels:
1115 for episode in channel.get_new_episodes():
1116 episodes.append(episode)
1117 self.new_episodes_show(episodes)
1119 def on_sync_to_ipod_activate(self, widget, episodes=None):
1120 Thread(target=self.sync_to_ipod_thread, args=(widget, episodes)).start()
1122 def sync_to_ipod_thread(self, widget, episodes=None):
1123 device = sync.open_device()
1125 if device is None:
1126 title = _('No device configured')
1127 message = _('To use the synchronization feature, please configure your device in the preferences dialog first.')
1128 self.notification(message, title)
1129 return
1131 if not device.open():
1132 title = _('Cannot open device')
1133 message = _('There has been an error opening your device.')
1134 self.notification(message, title)
1135 return
1137 gPodderSync(device=device, gPodder=self)
1138 if self.tray_icon:
1139 self.tray_icon.set_synchronisation_device(device)
1141 if episodes is None:
1142 episodes_to_sync = []
1143 for channel in self.channels:
1144 if not channel.sync_to_devices:
1145 log('Skipping channel: %s', channel.title, sender=self)
1146 continue
1148 for episode in channel.get_all_episodes():
1149 if episode.is_downloaded():
1150 episodes_to_sync.append(episode)
1151 device.add_tracks(episodes_to_sync)
1152 else:
1153 device.add_tracks(episodes, force_played=True)
1155 if not device.close():
1156 title = _('Error closing device')
1157 message = _('There has been an error closing your device.')
1158 self.notification(message, title)
1159 return
1161 if self.tray_icon:
1162 self.tray_icon.release_synchronisation_device()
1164 # update model for played state updates after sync
1165 for channel in self.channels:
1166 util.idle_add(channel.update_model)
1167 util.idle_add(self.updateComboBox)
1169 def ipod_cleanup_callback(self, device, tracks):
1170 title = _('Delete podcasts from device?')
1171 message = _('Do you really want to completely remove the selected episodes?')
1172 if len(tracks) > 0 and self.show_confirmation(message, title):
1173 device.remove_tracks(tracks)
1175 if not device.close():
1176 title = _('Error closing device')
1177 message = _('There has been an error closing your device.')
1178 self.show_message(message, title)
1179 return
1181 def on_cleanup_ipod_activate(self, widget, *args):
1182 columns = (
1183 ('title', _('Episode')),
1184 ('filesize', _('Size')),
1185 ('modified', _('Copied')),
1188 device = sync.open_device()
1190 if device is None:
1191 title = _('No device configured')
1192 message = _('To use the synchronization feature, please configure your device in the preferences dialog first.')
1193 self.show_message(message, title)
1194 return
1196 if not device.open():
1197 title = _('Cannot open device')
1198 message = _('There has been an error opening your device.')
1199 self.show_message(message, title)
1200 return
1202 gPodderSync(device=device, gPodder=self)
1204 tracks = device.get_all_tracks()
1205 if len(tracks) > 0:
1206 remove_tracks_callback = lambda tracks: self.ipod_cleanup_callback(device, tracks)
1207 title = _('Remove podcasts from device')
1208 instructions = _('Select the podcast episodes you want to remove from your device.')
1209 gPodderEpisodeSelector(title=title, instructions=instructions, episodes=tracks, columns=columns, \
1210 stock_ok_button=gtk.STOCK_DELETE, callback=remove_tracks_callback)
1211 else:
1212 title = _('No files on device')
1213 message = _('The devices contains no files to be removed.')
1214 self.show_message(message, title)
1216 def show_hide_tray_icon(self):
1217 gl = gPodderLib()
1219 if gl.config.display_tray_icon and self.tray_icon is None:
1220 self.tray_icon = trayicon.GPodderStatusIcon(self, scalable_dir)
1221 elif not gl.config.display_tray_icon and self.tray_icon is not None:
1222 self.tray_icon.set_visible(False)
1223 del self.tray_icon
1224 self.tray_icon = None
1226 if gl.config.minimize_to_tray and self.tray_icon:
1227 self.tray_icon.set_visible(self.minimized)
1228 elif self.tray_icon:
1229 self.tray_icon.set_visible(True)
1231 def update_view_settings(self):
1232 gl = gPodderLib()
1234 if gl.config.show_toolbar:
1235 self.toolbar.show_all()
1236 else:
1237 self.toolbar.hide_all()
1239 if self.episode_description_shown != gl.config.episode_list_descriptions:
1240 for channel in self.channels:
1241 channel.force_update_tree_model()
1242 self.updateTreeView()
1243 self.episode_description_shown = gl.config.episode_list_descriptions
1245 def on_itemShowToolbar_activate(self, widget):
1246 gl = gPodderLib()
1247 gl.config.show_toolbar = self.itemShowToolbar.get_active()
1248 self.update_view_settings()
1250 def on_itemShowDescription_activate(self, widget):
1251 gl = gPodderLib()
1252 gl.config.episode_list_descriptions = self.itemShowDescription.get_active()
1253 self.update_view_settings()
1255 def update_item_device( self):
1256 gl = gPodderLib()
1258 if gl.config.device_type != 'none':
1259 self.itemDevice.show_all()
1260 ( label, image ) = self.itemDevice.get_children()
1261 label.set_text( gl.get_device_name())
1262 else:
1263 self.itemDevice.hide_all()
1265 def properties_closed( self):
1266 self.show_hide_tray_icon()
1267 self.update_item_device()
1268 self.updateComboBox()
1270 def on_itemPreferences_activate(self, widget, *args):
1271 prop = gPodderProperties( callback_finished = self.properties_closed)
1272 prop.set_uar( self.user_apps_reader)
1273 prop.set_video_uar(self.user_video_apps_reader)
1275 def on_itemAddChannel_activate(self, widget, *args):
1276 if self.channelPaned.get_position() < 200:
1277 self.channelPaned.set_position( 200)
1278 self.entryAddChannel.set_text( _('Enter podcast URL'))
1279 self.entryAddChannel.grab_focus()
1281 def on_itemEditChannel_activate(self, widget, *args):
1282 if self.active_channel is None:
1283 title = _('No channel selected')
1284 message = _('Please select a channel in the channels list to edit.')
1285 self.show_message( message, title)
1286 return
1288 gPodderChannel( channel = self.active_channel, callback_closed = self.updateComboBox)
1290 def on_itemRemoveChannel_activate(self, widget, *args):
1291 try:
1292 dialog = gtk.MessageDialog(self.gPodder, gtk.DIALOG_MODAL, gtk.MESSAGE_QUESTION, gtk.BUTTONS_NONE)
1293 dialog.add_button(gtk.STOCK_NO, gtk.RESPONSE_NO)
1294 dialog.add_button(gtk.STOCK_YES, gtk.RESPONSE_YES)
1296 title = _('Remove channel and episodes?')
1297 message = _('Do you really want to remove <b>%s</b> and all downloaded episodes?') % saxutils.escape(self.active_channel.title)
1299 dialog.set_title(title)
1300 dialog.set_markup('<span weight="bold" size="larger">%s</span>\n\n%s'%(title, message))
1302 cb_ask = gtk.CheckButton(_('Do not delete my downloaded episodes'))
1303 dialog.vbox.pack_start(cb_ask)
1304 cb_ask.show_all()
1306 result = dialog.run()
1307 dialog.destroy()
1309 if result == gtk.RESPONSE_YES:
1310 # delete downloaded episodes only if checkbox is unchecked
1311 if cb_ask.get_active() == False:
1312 self.active_channel.remove_downloaded()
1313 else:
1314 log('Not removing downloaded episodes', sender=self)
1316 # only delete partial files if we do not have any downloads in progress
1317 delete_partial = not services.download_status_manager.has_items()
1318 gPodderLib().clean_up_downloads(delete_partial)
1319 self.channels.remove(self.active_channel)
1320 save_channels(self.channels)
1321 if len(self.channels) > 0:
1322 self.treeChannels.get_selection().select_path((len(self.channels)-1,))
1323 self.active_channel = self.channels[len(self.channels)-1]
1324 self.update_feed_cache(force_update=False)
1325 except:
1326 log('There has been an error removing the channel.', traceback=True, sender=self)
1328 def on_itemExportChannels_activate(self, widget, *args):
1329 if not self.channels:
1330 title = _('Nothing to export')
1331 message = _('Your list of channel subscriptions is empty. Please subscribe to some podcasts first before trying to export your subscription list.')
1332 self.show_message( message, title)
1333 return
1335 dlg = gtk.FileChooserDialog( title=_("Export to OPML"), parent = None, action = gtk.FILE_CHOOSER_ACTION_SAVE)
1336 dlg.add_button( gtk.STOCK_CANCEL, gtk.RESPONSE_CANCEL)
1337 dlg.add_button( gtk.STOCK_SAVE, gtk.RESPONSE_OK)
1338 response = dlg.run()
1339 if response == gtk.RESPONSE_OK:
1340 filename = dlg.get_filename()
1341 exporter = opml.Exporter( filename)
1342 if not exporter.write( self.channels):
1343 self.show_message( _('Could not export OPML to file. Please check your permissions.'), _('OPML export failed'))
1345 dlg.destroy()
1347 def on_itemImportChannels_activate(self, widget, *args):
1348 gPodderOpmlLister().get_channels_from_url( gPodderLib().config.opml_url, lambda url: self.add_new_channel(url,False), lambda: self.on_itemDownloadAllNew_activate( self.gPodder))
1350 def on_btnTransfer_clicked(self, widget, *args):
1351 self.on_treeAvailable_row_activated( widget, args)
1353 def on_homepage_activate(self, widget, *args):
1354 Thread( target = webbrowser.open, args = ( app_website, )).start()
1356 def on_wishlist_activate(self, widget, *args):
1357 Thread( target = webbrowser.open, args = ( 'http://www.amazon.de/gp/registry/2PD2MYGHE6857', )).start()
1359 def on_wiki_activate(self, widget, *args):
1360 Thread(target=webbrowser.open, args=('http://gpodderwiki.jottit.com/',)).start()
1362 def on_bug_tracker_activate(self, widget, *args):
1363 Thread(target=webbrowser.open, args=('http://gpodder.thegithouse.com/',)).start()
1365 def on_itemAbout_activate(self, widget, *args):
1366 dlg = gtk.AboutDialog()
1367 dlg.set_name( app_name)
1368 dlg.set_version( app_version)
1369 dlg.set_authors( app_authors)
1370 dlg.set_copyright( app_copyright)
1371 dlg.set_website( app_website)
1372 dlg.set_translator_credits( _('translator-credits'))
1373 dlg.connect( 'response', lambda dlg, response: dlg.destroy())
1375 try:
1376 dlg.set_logo( gtk.gdk.pixbuf_new_from_file_at_size( scalable_dir, 200, 200))
1377 except:
1378 pass
1380 dlg.run()
1382 def on_wNotebook_switch_page(self, widget, *args):
1383 page_num = args[1]
1384 if page_num == 0:
1385 self.play_or_download()
1386 else:
1387 self.toolDownload.set_sensitive( False)
1388 self.toolPlay.set_sensitive( False)
1389 self.toolTransfer.set_sensitive( False)
1390 self.toolCancel.set_sensitive( services.download_status_manager.has_items())
1392 def on_treeChannels_row_activated(self, widget, *args):
1393 self.on_itemEditChannel_activate( self.treeChannels)
1395 def on_treeChannels_cursor_changed(self, widget, *args):
1396 ( model, iter ) = self.treeChannels.get_selection().get_selected()
1398 if model != None and iter != None:
1399 id = model.get_path( iter)[0]
1400 self.active_channel = self.channels[id]
1402 self.itemEditChannel.get_child().set_text( _('Edit "%s"') % ( self.active_channel.title,))
1403 self.itemRemoveChannel.get_child().set_text( _('Remove "%s"') % ( self.active_channel.title,))
1404 self.itemEditChannel.show_all()
1405 self.itemRemoveChannel.show_all()
1406 else:
1407 self.active_channel = None
1408 self.itemEditChannel.hide_all()
1409 self.itemRemoveChannel.hide_all()
1411 self.updateTreeView()
1413 def on_entryAddChannel_changed(self, widget, *args):
1414 active = self.entryAddChannel.get_text() not in ('', _('Enter podcast URL'))
1415 self.btnAddChannel.set_sensitive( active)
1417 def on_btnAddChannel_clicked(self, widget, *args):
1418 url = self.entryAddChannel.get_text()
1419 self.entryAddChannel.set_text('')
1420 self.add_new_channel( url)
1422 def on_btnEditChannel_clicked(self, widget, *args):
1423 self.on_itemEditChannel_activate( widget, args)
1425 def on_treeAvailable_row_activated(self, widget, *args):
1426 try:
1427 selection = self.treeAvailable.get_selection()
1428 selection_tuple = selection.get_selected_rows()
1429 transfer_files = False
1430 episodes = []
1432 if selection.count_selected_rows() > 1:
1433 widget_to_send = None
1434 show_message_dialog = False
1435 else:
1436 widget_to_send = widget
1437 show_message_dialog = True
1439 if widget.get_name() == 'itemTransferSelected' or widget.get_name() == 'toolTransfer':
1440 transfer_files = True
1442 for apath in selection_tuple[1]:
1443 selection_iter = self.treeAvailable.get_model().get_iter( apath)
1444 url = self.treeAvailable.get_model().get_value( selection_iter, 0)
1446 if transfer_files:
1447 episodes.append( self.active_channel.find_episode( url))
1448 else:
1449 self.download_podcast_by_url( url, show_message_dialog, widget_to_send)
1451 if transfer_files and len(episodes):
1452 self.on_sync_to_ipod_activate(None, episodes)
1453 except:
1454 title = _('Nothing selected')
1455 message = _('Please select an episode that you want to download and then click on the download button to start downloading the selected episode.')
1456 self.show_message( message, title)
1458 def on_btnDownload_clicked(self, widget, *args):
1459 self.on_treeAvailable_row_activated( widget, args)
1461 def on_treeAvailable_button_release_event(self, widget, *args):
1462 self.play_or_download()
1464 def on_btnDownloadNewer_clicked(self, widget, *args):
1465 self.new_episodes_show(self.active_channel.get_new_episodes())
1467 def on_btnSelectAllAvailable_clicked(self, widget, *args):
1468 self.treeAvailable.get_selection().select_all()
1469 self.on_treeAvailable_row_activated( self.toolDownload, args)
1470 self.treeAvailable.get_selection().unselect_all()
1472 def auto_update_procedure(self, first_run=False):
1473 log('auto_update_procedure() got called', sender=self)
1474 gl = gPodderLib()
1475 if not first_run and gl.config.auto_update_feeds and self.minimized:
1476 self.update_feed_cache()
1478 next_update = 60*1000*gl.config.auto_update_frequency
1479 gobject.timeout_add(next_update, self.auto_update_procedure)
1481 def on_treeDownloads_row_activated(self, widget, *args):
1482 cancel_urls = []
1484 if self.wNotebook.get_current_page() > 0:
1485 # Use the download list treeview + model
1486 ( tree, column ) = ( self.treeDownloads, 3 )
1487 else:
1488 # Use the available podcasts treeview + model
1489 ( tree, column ) = ( self.treeAvailable, 0 )
1491 selection = tree.get_selection()
1492 (model, paths) = selection.get_selected_rows()
1493 for path in paths:
1494 url = model.get_value( model.get_iter( path), column)
1495 cancel_urls.append( url)
1497 if len( cancel_urls) == 0:
1498 log('Nothing selected.', sender = self)
1499 return
1501 if len( cancel_urls) == 1:
1502 title = _('Cancel download?')
1503 message = _("Cancelling this download will remove the partially downloaded file and stop the download.")
1504 else:
1505 title = _('Cancel downloads?')
1506 message = _("Cancelling the download will stop the %d selected downloads and remove partially downloaded files.") % selection.count_selected_rows()
1508 if self.show_confirmation( message, title):
1509 for url in cancel_urls:
1510 services.download_status_manager.cancel_by_url( url)
1512 def on_btnCancelDownloadStatus_clicked(self, widget, *args):
1513 self.on_treeDownloads_row_activated( widget, None)
1515 def on_btnCancelAll_clicked(self, widget, *args):
1516 self.treeDownloads.get_selection().select_all()
1517 self.on_treeDownloads_row_activated( self.toolCancel, None)
1518 self.treeDownloads.get_selection().unselect_all()
1520 def on_btnDownloadedExecute_clicked(self, widget, *args):
1521 self.on_treeAvailable_row_activated( widget, args)
1523 def on_btnDownloadedDelete_clicked(self, widget, *args):
1524 if self.active_channel is None:
1525 return
1527 channel_url = self.active_channel.url
1528 selection = self.treeAvailable.get_selection()
1529 ( model, paths ) = selection.get_selected_rows()
1531 if selection.count_selected_rows() == 0:
1532 log( 'Nothing selected - will not remove any downloaded episode.')
1533 return
1535 if selection.count_selected_rows() == 1:
1536 episode_title = saxutils.escape(model.get_value(model.get_iter(paths[0]), 1))
1538 locked = gPodderLib().history_is_locked(model.get_value(model.get_iter(paths[0]), 0))
1539 if locked:
1540 title = _('%s is locked') % episode_title
1541 message = _('You cannot delete this locked episode. You must unlock it before you can delete it.')
1542 self.notification(message, title)
1543 return
1545 title = _('Remove %s?') % episode_title
1546 message = _("If you remove this episode, it will be deleted from your computer. If you want to listen to this episode again, you will have to re-download it.")
1547 else:
1548 title = _('Remove %d episodes?') % selection.count_selected_rows()
1549 message = _('If you remove these episodes, they will be deleted from your computer. If you want to listen to any of these episodes again, you will have to re-download the episodes in question.')
1551 locked_count = 0
1552 for path in paths:
1553 url = model.get_value(model.get_iter(path), 0)
1554 if gPodderLib().history_is_locked(url):
1555 locked_count += 1
1557 if selection.count_selected_rows() == locked_count:
1558 title = _('Episodes are locked')
1559 message = _('The selected episodes are locked. Please unlock the episodes that you want to delete before trying to delete them.')
1560 self.notification(message, title)
1561 return
1562 elif locked_count > 0:
1563 title = _('Remove %d out of %d episodes?') % (selection.count_selected_rows() - locked_count, selection.count_selected_rows())
1564 message = _('The selection contains locked episodes. These will not be deleted. If you want to listen to any of these episodes again, then you will have to re-download them.')
1566 # if user confirms deletion, let's remove some stuff ;)
1567 if self.show_confirmation( message, title):
1568 try:
1569 # iterate over the selection, see also on_treeDownloads_row_activated
1570 for path in paths:
1571 url = model.get_value( model.get_iter( path), 0)
1572 self.active_channel.delete_episode_by_url( url)
1573 gPodderLib().history_mark_downloaded( url)
1575 # now, clear local db cache so we can re-read it
1576 self.updateComboBox()
1577 except:
1578 log( 'Error while deleting (some) downloads.')
1580 # only delete partial files if we do not have any downloads in progress
1581 delete_partial = not services.download_status_manager.has_items()
1582 gPodderLib().clean_up_downloads( delete_partial)
1583 self.active_channel.force_update_tree_model()
1584 self.updateTreeView()
1586 def on_btnDeleteAll_clicked(self, widget, *args):
1587 self.treeAvailable.get_selection().select_all()
1588 self.on_btnDownloadedDelete_clicked( widget, args)
1589 self.treeAvailable.get_selection().unselect_all()
1591 def window_state_event(self, widget, event):
1592 old_minimized = self.minimized
1594 if event.new_window_state & gtk.gdk.WINDOW_STATE_ICONIFIED:
1595 self.minimized = True
1596 else:
1597 self.minimized = False
1599 if old_minimized != self.minimized and self.tray_icon:
1600 self.gPodder.set_skip_taskbar_hint(self.minimized)
1601 elif not self.tray_icon:
1602 self.gPodder.set_skip_taskbar_hint(False)
1604 gl = gPodderLib()
1605 if gl.config.minimize_to_tray and self.tray_icon:
1606 self.tray_icon.set_visible(self.minimized)
1608 def uniconify_main_window(self):
1609 if self.minimized:
1610 self.gPodder.present()
1612 def iconify_main_window(self):
1613 if not self.minimized:
1614 self.gPodder.iconify()
1616 class gPodderChannel(GladeWidget):
1617 def new(self):
1618 self.gPodderChannel.set_title( self.channel.title)
1619 self.entryTitle.set_text( self.channel.title)
1620 self.entryURL.set_text( self.channel.url)
1622 self.LabelDownloadTo.set_text( self.channel.save_dir)
1623 self.LabelWebsite.set_text( self.channel.link)
1625 self.channel.load_settings()
1626 self.cbNoSync.set_active( not self.channel.sync_to_devices)
1627 self.musicPlaylist.set_text(self.channel.device_playlist_name)
1628 if self.channel.username:
1629 self.FeedUsername.set_text( self.channel.username)
1630 if self.channel.password:
1631 self.FeedPassword.set_text( self.channel.password)
1633 self.on_btnClearCover_clicked( self.btnClearCover, delete_file = False)
1634 self.on_btnDownloadCover_clicked( self.btnDownloadCover, url = False)
1636 b = gtk.TextBuffer()
1637 b.set_text( self.channel.description)
1638 self.channel_description.set_buffer( b)
1640 #Add Drag and Drop Support
1641 flags = gtk.DEST_DEFAULT_ALL
1642 targets = [ ('text/uri-list', 0, 2), ('text/plain', 0, 4) ]
1643 actions = gtk.gdk.ACTION_DEFAULT | gtk.gdk.ACTION_COPY
1644 self.vboxCoverEditor.drag_dest_set( flags, targets, actions)
1645 self.vboxCoverEditor.connect( 'drag_data_received', self.drag_data_received)
1647 def on_btnClearCover_clicked( self, widget, delete_file = True):
1648 self.imgCover.clear()
1649 if delete_file:
1650 util.delete_file( self.channel.cover_file)
1651 self.btnClearCover.set_sensitive( os.path.exists( self.channel.cover_file))
1652 self.btnDownloadCover.set_sensitive( not os.path.exists( self.channel.cover_file) and bool(self.channel.image))
1653 self.labelCoverStatus.set_text( _('You can drag a cover file here.'))
1654 self.labelCoverStatus.show()
1656 def on_btnDownloadCover_clicked( self, widget, url = None):
1657 if url is None:
1658 url = self.channel.image
1660 if url != False:
1661 self.btnDownloadCover.set_sensitive( False)
1663 self.labelCoverStatus.show()
1664 gPodderLib().get_image_from_url( url, self.imgCover.set_from_pixbuf, self.labelCoverStatus.set_text, self.cover_download_finished, self.channel.cover_file)
1666 def cover_download_finished( self):
1667 self.labelCoverStatus.hide()
1668 self.btnClearCover.set_sensitive( os.path.exists( self.channel.cover_file))
1669 self.btnDownloadCover.set_sensitive( not os.path.exists( self.channel.cover_file) and bool(self.channel.image))
1671 def drag_data_received( self, widget, content, x, y, sel, ttype, time):
1672 files = sel.data.strip().split('\n')
1673 if len(files) != 1:
1674 self.show_message( _('You can only drop a single image or URL here.'), _('Drag and drop'))
1675 return
1677 file = files[0]
1679 if file.startswith( 'file://') or file.startswith( 'http://'):
1680 self.on_btnClearCover_clicked( self.btnClearCover)
1681 if file.startswith( 'file://'):
1682 filename = file[len('file://'):]
1683 shutil.copyfile( filename, self.channel.cover_file)
1684 self.on_btnDownloadCover_clicked( self.btnDownloadCover, url = file)
1685 return
1687 self.show_message( _('You can only drop local files and http:// URLs here.'), _('Drag and drop'))
1689 def on_gPodderChannel_destroy(self, widget, *args):
1690 self.callback_closed()
1692 def on_btnOK_clicked(self, widget, *args):
1693 self.channel.sync_to_devices = not self.cbNoSync.get_active()
1694 self.channel.device_playlist_name = self.musicPlaylist.get_text()
1695 self.channel.set_custom_title( self.entryTitle.get_text())
1696 self.channel.username = self.FeedUsername.get_text().strip()
1697 self.channel.password = self.FeedPassword.get_text()
1698 self.channel.save_settings()
1700 self.gPodderChannel.destroy()
1703 class gPodderProperties(GladeWidget):
1704 def new(self):
1705 if not hasattr( self, 'callback_finished'):
1706 self.callback_finished = None
1708 gl = gPodderLib()
1710 gl.config.connect_gtk_editable( 'http_proxy', self.httpProxy)
1711 gl.config.connect_gtk_editable( 'ftp_proxy', self.ftpProxy)
1712 gl.config.connect_gtk_editable( 'player', self.openApp)
1713 gl.config.connect_gtk_editable('videoplayer', self.openVideoApp)
1714 gl.config.connect_gtk_editable( 'opml_url', self.opmlURL)
1715 gl.config.connect_gtk_editable( 'custom_sync_name', self.entryCustomSyncName)
1716 gl.config.connect_gtk_togglebutton( 'custom_sync_name_enabled', self.cbCustomSyncName)
1717 gl.config.connect_gtk_togglebutton( 'auto_download_when_minimized', self.downloadnew)
1718 gl.config.connect_gtk_togglebutton( 'use_gnome_bittorrent', self.radio_gnome_bittorrent)
1719 gl.config.connect_gtk_togglebutton( 'update_on_startup', self.updateonstartup)
1720 gl.config.connect_gtk_togglebutton( 'only_sync_not_played', self.only_sync_not_played)
1721 gl.config.connect_gtk_togglebutton( 'fssync_channel_subfolders', self.cbChannelSubfolder)
1722 gl.config.connect_gtk_togglebutton( 'on_sync_mark_played', self.on_sync_mark_played)
1723 gl.config.connect_gtk_togglebutton( 'on_sync_delete', self.on_sync_delete)
1724 gl.config.connect_gtk_spinbutton( 'max_downloads', self.spinMaxDownloads)
1725 gl.config.connect_gtk_togglebutton( 'max_downloads_enabled', self.cbMaxDownloads)
1726 gl.config.connect_gtk_spinbutton( 'limit_rate_value', self.spinLimitDownloads)
1727 gl.config.connect_gtk_togglebutton( 'limit_rate', self.cbLimitDownloads)
1728 gl.config.connect_gtk_togglebutton( 'proxy_use_environment', self.cbEnvironmentVariables)
1729 gl.config.connect_gtk_filechooser( 'bittorrent_dir', self.chooserBitTorrentTo)
1730 gl.config.connect_gtk_spinbutton('episode_old_age', self.episode_old_age)
1731 gl.config.connect_gtk_togglebutton('auto_remove_old_episodes', self.auto_remove_old_episodes)
1732 gl.config.connect_gtk_togglebutton('auto_update_feeds', self.auto_update_feeds)
1733 gl.config.connect_gtk_spinbutton('auto_update_frequency', self.auto_update_frequency)
1734 gl.config.connect_gtk_togglebutton('display_tray_icon', self.display_tray_icon)
1735 gl.config.connect_gtk_togglebutton('minimize_to_tray', self.minimize_to_tray)
1736 gl.config.connect_gtk_togglebutton('enable_notifications', self.enable_notifications)
1737 gl.config.connect_gtk_togglebutton('start_iconified', self.start_iconified)
1738 gl.config.connect_gtk_togglebutton('on_quit_ask', self.on_quit_ask)
1739 gl.config.connect_gtk_togglebutton('bluetooth_enabled', self.bluetooth_enabled)
1740 gl.config.connect_gtk_togglebutton('bluetooth_ask_always', self.bluetooth_ask_always)
1741 gl.config.connect_gtk_togglebutton('bluetooth_ask_never', self.bluetooth_ask_never)
1742 gl.config.connect_gtk_togglebutton('bluetooth_use_converter', self.bluetooth_use_converter)
1743 gl.config.connect_gtk_filechooser( 'bluetooth_converter', self.bluetooth_converter, is_for_files=True)
1745 self.enable_notifications.set_sensitive(self.display_tray_icon.get_active())
1746 self.minimize_to_tray.set_sensitive(self.display_tray_icon.get_active())
1748 self.entryCustomSyncName.set_sensitive( self.cbCustomSyncName.get_active())
1749 self.radio_copy_torrents.set_active( not self.radio_gnome_bittorrent.get_active())
1751 self.iPodMountpoint.set_label( gl.config.ipod_mount)
1752 self.filesystemMountpoint.set_label( gl.config.mp3_player_folder)
1753 self.bluetooth_device_name.set_markup('<b>%s</b>'%gl.config.bluetooth_device_name)
1754 self.chooserDownloadTo.set_current_folder(gl.downloaddir)
1756 if tagging_supported():
1757 gl.config.connect_gtk_togglebutton( 'update_tags', self.updatetags)
1758 else:
1759 self.updatetags.set_sensitive( False)
1760 new_label = '%s (%s)' % ( self.updatetags.get_label(), _('needs python-eyed3') )
1761 self.updatetags.set_label( new_label)
1763 # device type
1764 self.comboboxDeviceType.set_active( 0)
1765 if gl.config.device_type == 'ipod':
1766 self.comboboxDeviceType.set_active( 1)
1767 elif gl.config.device_type == 'filesystem':
1768 self.comboboxDeviceType.set_active( 2)
1770 # setup cell renderers
1771 cellrenderer = gtk.CellRendererPixbuf()
1772 self.comboAudioPlayerApp.pack_start(cellrenderer, False)
1773 self.comboAudioPlayerApp.add_attribute(cellrenderer, 'pixbuf', 2)
1774 cellrenderer = gtk.CellRendererText()
1775 self.comboAudioPlayerApp.pack_start(cellrenderer, True)
1776 self.comboAudioPlayerApp.add_attribute(cellrenderer, 'markup', 0)
1778 cellrenderer = gtk.CellRendererPixbuf()
1779 self.comboVideoPlayerApp.pack_start(cellrenderer, False)
1780 self.comboVideoPlayerApp.add_attribute(cellrenderer, 'pixbuf', 2)
1781 cellrenderer = gtk.CellRendererText()
1782 self.comboVideoPlayerApp.pack_start(cellrenderer, True)
1783 self.comboVideoPlayerApp.add_attribute(cellrenderer, 'markup', 0)
1785 self.ipodIcon.set_from_icon_name( 'gnome-dev-ipod', gtk.ICON_SIZE_BUTTON)
1787 def update_mountpoint( self, ipod):
1788 if ipod is None or ipod.mount_point is None:
1789 self.iPodMountpoint.set_label( '')
1790 else:
1791 self.iPodMountpoint.set_label( ipod.mount_point)
1793 def on_bluetooth_select_device_clicked(self, widget):
1794 gl = gPodderLib()
1795 self.bluetooth_select_device.set_sensitive(False)
1796 gtk.main_iteration(False)
1797 for name, address in util.discover_bluetooth_devices():
1798 if self.show_confirmation('Use this device as your bluetooth device?', name):
1799 gl.config.bluetooth_device_name = name
1800 gl.config.bluetooth_device_address = address
1801 self.bluetooth_device_name.set_markup('<b>%s</b>'%gl.config.bluetooth_device_name)
1802 self.bluetooth_select_device.set_sensitive(True)
1803 return
1804 self.show_message('No more devices found', 'Scan finished')
1805 self.bluetooth_select_device.set_sensitive(True)
1807 def set_uar(self, uar):
1808 self.comboAudioPlayerApp.set_model(uar.get_applications_as_model())
1809 index = self.find_active_audio_app()
1810 self.comboAudioPlayerApp.set_active(index)
1812 def set_video_uar(self, uar):
1813 self.comboVideoPlayerApp.set_model(uar.get_applications_as_model())
1814 index = self.find_active_video_app()
1815 self.comboVideoPlayerApp.set_active(index)
1817 def find_active_audio_app(self):
1818 model = self.comboAudioPlayerApp.get_model()
1819 iter = model.get_iter_first()
1820 index = 0
1821 while iter is not None:
1822 command = model.get_value(iter, 1)
1823 if command == self.openApp.get_text():
1824 return index
1825 iter = model.iter_next(iter)
1826 index += 1
1827 # return last item = custom command
1828 return index-1
1830 def find_active_video_app( self):
1831 model = self.comboVideoPlayerApp.get_model()
1832 iter = model.get_iter_first()
1833 index = 0
1834 while iter is not None:
1835 command = model.get_value(iter, 1)
1836 if command == self.openVideoApp.get_text():
1837 return index
1838 iter = model.iter_next(iter)
1839 index += 1
1840 # return last item = custom command
1841 return index-1
1843 def set_download_dir( self, new_download_dir, event = None):
1844 gl = gPodderLib()
1845 gl.downloaddir = self.chooserDownloadTo.get_filename()
1846 if gl.downloaddir != self.chooserDownloadTo.get_filename():
1847 self.notification(_('There has been an error moving your downloads to the specified location. The old download directory will be used instead.'), _('Error moving downloads'))
1849 if event:
1850 event.set()
1852 def on_auto_update_feeds_toggled( self, widget, *args):
1853 self.auto_update_frequency.set_sensitive(widget.get_active())
1855 def on_display_tray_icon_toggled( self, widget, *args):
1856 self.enable_notifications.set_sensitive(widget.get_active())
1857 self.minimize_to_tray.set_sensitive(widget.get_active())
1859 def on_cbCustomSyncName_toggled( self, widget, *args):
1860 self.entryCustomSyncName.set_sensitive( widget.get_active())
1862 def on_btnCustomSyncNameHelp_clicked( self, widget):
1863 examples = [
1864 '<i>{episode.title}</i> -&gt; <b>Interview with RMS</b>',
1865 '<i>{episode.basename}</i> -&gt; <b>70908-interview-rms</b>',
1866 '<i>{episode.published}</i> -&gt; <b>20070908</b>'
1869 info = [
1870 _('You can specify a custom format string for the file names on your MP3 player here.'),
1871 _('The format string will be used to generate a file name on your device. The file extension (e.g. ".mp3") will be added automatically.'),
1872 '\n'.join( [ ' %s' % s for s in examples ])
1875 self.show_message( '\n\n'.join( info), _('Custom format strings'))
1877 def on_gPodderProperties_destroy(self, widget, *args):
1878 self.on_btnOK_clicked( widget, *args)
1880 def on_btnConfigEditor_clicked(self, widget, *args):
1881 self.on_btnOK_clicked(widget, *args)
1882 gPodderConfigEditor()
1884 def on_comboAudioPlayerApp_changed(self, widget, *args):
1885 # find out which one
1886 iter = self.comboAudioPlayerApp.get_active_iter()
1887 model = self.comboAudioPlayerApp.get_model()
1888 command = model.get_value( iter, 1)
1889 if command == '':
1890 self.openApp.set_sensitive( True)
1891 self.openApp.show()
1892 self.labelCustomCommand.show()
1893 else:
1894 self.openApp.set_text( command)
1895 self.openApp.set_sensitive( False)
1896 self.openApp.hide()
1897 self.labelCustomCommand.hide()
1899 def on_comboVideoPlayerApp_changed(self, widget, *args):
1900 # find out which one
1901 iter = self.comboVideoPlayerApp.get_active_iter()
1902 model = self.comboVideoPlayerApp.get_model()
1903 command = model.get_value(iter, 1)
1904 if command == '':
1905 self.openVideoApp.set_sensitive(True)
1906 self.openVideoApp.show()
1907 self.labelCustomCommand.show()
1908 else:
1909 self.openVideoApp.set_text(command)
1910 self.openVideoApp.set_sensitive(False)
1911 self.openVideoApp.hide()
1912 self.labelCustomCommand.hide()
1914 def on_cbMaxDownloads_toggled(self, widget, *args):
1915 self.spinMaxDownloads.set_sensitive( self.cbMaxDownloads.get_active())
1917 def on_cbLimitDownloads_toggled(self, widget, *args):
1918 self.spinLimitDownloads.set_sensitive( self.cbLimitDownloads.get_active())
1920 def on_cbEnvironmentVariables_toggled(self, widget, *args):
1921 sens = not self.cbEnvironmentVariables.get_active()
1922 self.httpProxy.set_sensitive( sens)
1923 self.ftpProxy.set_sensitive( sens)
1925 def on_comboboxDeviceType_changed(self, widget, *args):
1926 active_item = self.comboboxDeviceType.get_active()
1928 # None
1929 sync_widgets = ( self.only_sync_not_played, self.labelSyncOptions,
1930 self.imageSyncOptions, self. separatorSyncOptions,
1931 self.on_sync_mark_played, self.on_sync_delete,
1932 self.on_sync_leave, self.label_after_sync, )
1933 for widget in sync_widgets:
1934 if active_item == 0:
1935 widget.hide_all()
1936 else:
1937 widget.show_all()
1939 # iPod
1940 ipod_widgets = ( self.ipodLabel, self.btn_iPodMountpoint )
1941 for widget in ipod_widgets:
1942 if active_item == 1:
1943 widget.show_all()
1944 else:
1945 widget.hide_all()
1947 # filesystem-based MP3 player
1948 fs_widgets = ( self.filesystemLabel, self.btn_filesystemMountpoint,
1949 self.cbChannelSubfolder, self.cbCustomSyncName,
1950 self.entryCustomSyncName, self.btnCustomSyncNameHelp )
1951 for widget in fs_widgets:
1952 if active_item == 2:
1953 widget.show_all()
1954 else:
1955 widget.hide_all()
1957 def on_btn_iPodMountpoint_clicked(self, widget, *args):
1958 fs = gtk.FileChooserDialog( title = _('Select iPod mountpoint'), action = gtk.FILE_CHOOSER_ACTION_SELECT_FOLDER)
1959 fs.add_button( gtk.STOCK_CANCEL, gtk.RESPONSE_CANCEL)
1960 fs.add_button( gtk.STOCK_OPEN, gtk.RESPONSE_OK)
1961 gl = gPodderLib()
1962 fs.set_current_folder(self.iPodMountpoint.get_label())
1963 if fs.run() == gtk.RESPONSE_OK:
1964 self.iPodMountpoint.set_label( fs.get_filename())
1965 fs.destroy()
1967 def on_btn_FilesystemMountpoint_clicked(self, widget, *args):
1968 fs = gtk.FileChooserDialog( title = _('Select folder for MP3 player'), action = gtk.FILE_CHOOSER_ACTION_SELECT_FOLDER)
1969 fs.add_button( gtk.STOCK_CANCEL, gtk.RESPONSE_CANCEL)
1970 fs.add_button( gtk.STOCK_OPEN, gtk.RESPONSE_OK)
1971 gl = gPodderLib()
1972 fs.set_current_folder(self.filesystemMountpoint.get_label())
1973 if fs.run() == gtk.RESPONSE_OK:
1974 self.filesystemMountpoint.set_label( fs.get_filename())
1975 fs.destroy()
1977 def on_btnOK_clicked(self, widget, *args):
1978 gl = gPodderLib()
1979 gl.config.ipod_mount = self.iPodMountpoint.get_label()
1980 gl.config.mp3_player_folder = self.filesystemMountpoint.get_label()
1982 if gl.downloaddir != self.chooserDownloadTo.get_filename():
1983 new_download_dir = self.chooserDownloadTo.get_filename()
1984 download_dir_size = util.calculate_size( gl.downloaddir)
1985 download_dir_size_string = gl.format_filesize( download_dir_size)
1986 event = Event()
1988 dlg = gtk.Dialog( _('Moving downloads folder'), self.gPodderProperties)
1989 dlg.vbox.set_spacing( 5)
1990 dlg.set_border_width( 5)
1992 label = gtk.Label()
1993 label.set_line_wrap( True)
1994 label.set_markup( _('Moving downloads from <b>%s</b> to <b>%s</b>...') % ( saxutils.escape( gl.downloaddir), saxutils.escape( new_download_dir), ))
1995 myprogressbar = gtk.ProgressBar()
1997 # put it all together
1998 dlg.vbox.pack_start( label)
1999 dlg.vbox.pack_end( myprogressbar)
2001 # switch windows
2002 dlg.show_all()
2003 self.gPodderProperties.hide_all()
2005 # hide action area and separator line
2006 dlg.action_area.hide()
2007 dlg.set_has_separator( False)
2009 args = ( new_download_dir, event, )
2011 thread = Thread( target = self.set_download_dir, args = args)
2012 thread.start()
2014 while not event.isSet():
2015 new_download_dir_size = util.calculate_size( new_download_dir)
2016 if download_dir_size > 0:
2017 fract = (1.00*new_download_dir_size) / (1.00*download_dir_size)
2018 else:
2019 fract = 0.0
2020 if fract < 0.99:
2021 myprogressbar.set_text( _('%s of %s') % ( gl.format_filesize( new_download_dir_size), download_dir_size_string, ))
2022 else:
2023 myprogressbar.set_text( _('Finishing... please wait.'))
2024 myprogressbar.set_fraction( fract)
2025 event.wait( 0.1)
2026 while gtk.events_pending():
2027 gtk.main_iteration( False)
2029 dlg.destroy()
2031 device_type = self.comboboxDeviceType.get_active()
2032 if device_type == 0:
2033 gl.config.device_type = 'none'
2034 elif device_type == 1:
2035 gl.config.device_type = 'ipod'
2036 elif device_type == 2:
2037 gl.config.device_type = 'filesystem'
2038 self.gPodderProperties.destroy()
2039 if self.callback_finished:
2040 self.callback_finished()
2043 class gPodderEpisode(GladeWidget):
2044 def new(self):
2045 services.download_status_manager.register( 'list-changed', self.on_download_status_changed)
2046 services.download_status_manager.register( 'progress-detail', self.on_download_status_progress)
2048 self.episode_title.set_markup( '<span weight="bold" size="larger">%s</span>' % saxutils.escape( self.episode.title))
2050 b = gtk.TextBuffer()
2051 b.set_text( strip( self.episode.description))
2052 self.episode_description.set_buffer( b)
2054 self.gPodderEpisode.set_title( self.episode.title)
2055 self.LabelDownloadLink.set_text( self.episode.url)
2056 self.LabelWebsiteLink.set_text( self.episode.link)
2057 self.labelPubDate.set_text( self.episode.pubDate)
2059 self.channel_title.set_markup( _('<i>from %s</i>') % saxutils.escape( self.channel.title))
2061 self.hide_show_widgets()
2062 services.download_status_manager.request_progress_detail( self.episode.url)
2064 def on_btnCancel_clicked( self, widget):
2065 services.download_status_manager.cancel_by_url( self.episode.url)
2067 def on_gPodderEpisode_destroy( self, widget):
2068 services.download_status_manager.unregister( 'list-changed', self.on_download_status_changed)
2069 services.download_status_manager.unregister( 'progress-detail', self.on_download_status_progress)
2071 def on_download_status_changed( self):
2072 self.hide_show_widgets()
2074 def on_download_status_progress( self, url, progress, speed):
2075 if url == self.episode.url:
2076 progress = float(min(100.0,max(0.0,progress)))
2077 self.progress_bar.set_fraction(progress/100.0)
2078 self.progress_bar.set_text( 'Downloading: %d%% (%s)' % ( progress, speed, ))
2080 def hide_show_widgets( self):
2081 is_downloading = services.download_status_manager.is_download_in_progress( self.episode.url)
2082 if is_downloading:
2083 self.progress_bar.show_all()
2084 self.btnCancel.show_all()
2085 self.btnPlay.hide_all()
2086 self.btnSaveFile.hide_all()
2087 self.btnDownload.hide_all()
2088 else:
2089 self.progress_bar.hide_all()
2090 self.btnCancel.hide_all()
2091 if os.path.exists( self.episode.local_filename()):
2092 self.btnPlay.show_all()
2093 self.btnSaveFile.show_all()
2094 self.btnDownload.hide_all()
2095 else:
2096 self.btnPlay.hide_all()
2097 self.btnSaveFile.hide_all()
2098 self.btnDownload.show_all()
2100 def on_btnCloseWindow_clicked(self, widget, *args):
2101 self.gPodderEpisode.destroy()
2103 def on_btnDownload_clicked(self, widget, *args):
2104 if self.download_callback:
2105 self.download_callback()
2107 def on_btnPlay_clicked(self, widget, *args):
2108 if self.play_callback:
2109 self.play_callback()
2111 self.gPodderEpisode.destroy()
2113 def on_btnSaveFile_clicked(self, widget, *args):
2114 self.show_copy_dialog( src_filename = self.episode.local_filename(), dst_filename = self.episode.sync_filename())
2117 class gPodderSync(GladeWidget):
2118 def new(self):
2119 util.idle_add(self.imageSync.set_from_icon_name, 'gnome-dev-ipod', gtk.ICON_SIZE_DIALOG)
2121 self.device.register('progress', self.on_progress)
2122 self.device.register('sub-progress', self.on_sub_progress)
2123 self.device.register('status', self.on_status)
2124 self.device.register('done', self.on_done)
2126 def on_progress(self, pos, max):
2127 util.idle_add(self.progressbar.set_fraction, float(pos)/float(max))
2128 util.idle_add(self.progressbar.set_text, _('%d of %d done') % (pos, max))
2130 def on_sub_progress(self, percentage):
2131 util.idle_add(self.progressbar.set_text, _('Processing (%d%%)') % (percentage))
2133 def on_status(self, status):
2134 util.idle_add(self.status_label.set_markup, '<i>%s</i>' % saxutils.escape(status))
2136 def on_done(self):
2137 util.idle_add(self.gPodderSync.destroy)
2138 if not self.gPodder.minimized:
2139 util.idle_add(self.notification, _('Your device has been updated by gPodder.'), _('Operation finished'))
2141 def on_gPodderSync_destroy(self, widget, *args):
2142 self.device.unregister('progress', self.on_progress)
2143 self.device.unregister('sub-progress', self.on_sub_progress)
2144 self.device.unregister('status', self.on_status)
2145 self.device.unregister('done', self.on_done)
2146 self.device.cancel()
2148 def on_cancel_button_clicked(self, widget, *args):
2149 self.device.cancel()
2152 class gPodderOpmlLister(GladeWidget):
2153 def new(self):
2154 # initiate channels list
2155 self.channels = []
2156 self.callback_for_channel = None
2157 self.callback_finished = None
2159 togglecell = gtk.CellRendererToggle()
2160 togglecell.set_property( 'activatable', True)
2161 togglecell.connect( 'toggled', self.callback_edited)
2162 togglecolumn = gtk.TreeViewColumn( '', togglecell, active=0)
2164 titlecell = gtk.CellRendererText()
2165 titlecolumn = gtk.TreeViewColumn( _('Channel'), titlecell, markup=1)
2167 for itemcolumn in ( togglecolumn, titlecolumn ):
2168 self.treeviewChannelChooser.append_column( itemcolumn)
2170 def callback_edited( self, cell, path):
2171 model = self.treeviewChannelChooser.get_model()
2173 url = model[path][2]
2175 model[path][0] = not model[path][0]
2176 if model[path][0]:
2177 self.channels.append( url)
2178 else:
2179 self.channels.remove( url)
2181 self.btnOK.set_sensitive( bool(len(self.channels)))
2183 def thread_finished(self, model):
2184 self.treeviewChannelChooser.set_model(model)
2185 self.labelStatus.set_label('')
2186 self.btnDownloadOpml.set_sensitive(True)
2187 self.entryURL.set_sensitive(True)
2188 self.treeviewChannelChooser.set_sensitive(True)
2189 self.channels = []
2191 def thread_func(self):
2192 url = self.entryURL.get_text()
2193 importer = opml.Importer(url)
2194 model = importer.get_model()
2195 if len(model) == 0:
2196 self.notification(_('The specified URL does not provide any valid OPML podcast items.'), _('No feeds found'))
2197 util.idle_add(self.thread_finished, model)
2199 def get_channels_from_url( self, url, callback_for_channel = None, callback_finished = None):
2200 if callback_for_channel:
2201 self.callback_for_channel = callback_for_channel
2202 if callback_finished:
2203 self.callback_finished = callback_finished
2204 self.labelStatus.set_label( _('Downloading, please wait...'))
2205 self.entryURL.set_text( url)
2206 self.btnDownloadOpml.set_sensitive( False)
2207 self.entryURL.set_sensitive( False)
2208 self.btnOK.set_sensitive( False)
2209 self.treeviewChannelChooser.set_sensitive( False)
2210 Thread( target = self.thread_func).start()
2212 def on_gPodderOpmlLister_destroy(self, widget, *args):
2213 pass
2215 def on_btnDownloadOpml_clicked(self, widget, *args):
2216 self.get_channels_from_url( self.entryURL.get_text())
2218 def on_btnOK_clicked(self, widget, *args):
2219 self.gPodderOpmlLister.destroy()
2221 # add channels that have been selected
2222 for url in self.channels:
2223 if self.callback_for_channel:
2224 self.callback_for_channel( url)
2226 if self.callback_finished:
2227 self.callback_finished()
2229 def on_btnCancel_clicked(self, widget, *args):
2230 self.gPodderOpmlLister.destroy()
2233 class gPodderEpisodeSelector( GladeWidget):
2234 """Episode selection dialog
2236 Optional keyword arguments that modify the behaviour of this dialog:
2238 - callback: Function that takes 1 parameter which is a list of
2239 the selected episodes (or empty list when none selected)
2240 - episodes: List of episodes that are presented for selection
2241 - selected: (optional) List of boolean variables that define the
2242 default checked state for the given episodes
2243 - selected_default: (optional) The default boolean value for the
2244 checked state if no other value is set
2245 (default is False)
2246 - columns: List of (name,caption) pairs for the columns, the name
2247 is the attribute name of the episode to be read from
2248 each episode object and the caption attribute is the
2249 text that appear as column caption
2250 (default is [('title','Episode'),])
2251 - title: (optional) The title of the window + heading
2252 - instructions: (optional) A one-line text describing what the
2253 user should select / what the selection is for
2254 - stock_ok_button: (optional) Will replace the "OK" button with
2255 another GTK+ stock item to be used for the
2256 affirmative button of the dialog (e.g. can
2257 be gtk.STOCK_DELETE when the episodes to be
2258 selected will be deleted after closing the
2259 dialog)
2260 - selection_buttons: (optional) A dictionary with labels as
2261 keys and callbacks as values; for each
2262 key a button will be generated, and when
2263 the button is clicked, the callback will
2264 be called for each episode and the return
2265 value of the callback (True or False) will
2266 be the new selected state of the episode
2267 - size_attribute: (optional) The name of an attribute of the
2268 supplied episode objects that can be used to
2269 calculate the size of an episode; set this to
2270 None if no total size calculation should be
2271 done (in cases where total size is useless)
2272 (default is 'length')
2275 COLUMN_TOGGLE = 0
2276 COLUMN_ADDITIONAL = 1
2278 def new( self):
2279 if not hasattr( self, 'callback'):
2280 self.callback = None
2282 if not hasattr( self, 'episodes'):
2283 self.episodes = []
2285 if not hasattr( self, 'size_attribute'):
2286 self.size_attribute = 'length'
2288 if not hasattr( self, 'selection_buttons'):
2289 self.selection_buttons = {}
2291 if not hasattr( self, 'selected_default'):
2292 self.selected_default = False
2294 if not hasattr( self, 'selected'):
2295 self.selected = [self.selected_default]*len(self.episodes)
2297 if len(self.selected) < len(self.episodes):
2298 self.selected += [self.selected_default]*(len(self.episodes)-len(self.selected))
2300 if not hasattr( self, 'columns'):
2301 self.columns = ( ('title', _('Episode')), )
2303 if hasattr( self, 'title'):
2304 self.gPodderEpisodeSelector.set_title( self.title)
2305 self.labelHeading.set_markup( '<b><big>%s</big></b>' % saxutils.escape( self.title))
2307 if hasattr( self, 'instructions'):
2308 self.labelInstructions.set_text( self.instructions)
2309 self.labelInstructions.show_all()
2311 if hasattr( self, 'stock_ok_button'):
2312 self.btnOK.set_label( self.stock_ok_button)
2313 self.btnOK.set_use_stock( True)
2315 toggle_cell = gtk.CellRendererToggle()
2316 toggle_cell.connect( 'toggled', self.toggle_cell_handler)
2318 self.treeviewEpisodes.append_column( gtk.TreeViewColumn( '', toggle_cell, active=self.COLUMN_TOGGLE))
2320 next_column = self.COLUMN_ADDITIONAL
2321 for name, caption in self.columns:
2322 renderer = gtk.CellRendererText()
2323 if next_column > self.COLUMN_ADDITIONAL:
2324 renderer.set_property( 'ellipsize', pango.ELLIPSIZE_END)
2325 column = gtk.TreeViewColumn( caption, renderer, text=next_column)
2326 column.set_resizable( True)
2327 column.set_expand( True)
2328 self.treeviewEpisodes.append_column( column)
2329 next_column += 1
2331 column_types = [ gobject.TYPE_BOOLEAN ] + [ gobject.TYPE_STRING ] * len(self.columns)
2332 self.model = gtk.ListStore( *column_types)
2334 for index, episode in enumerate( self.episodes):
2335 row = [ self.selected[index] ]
2336 for name, caption in self.columns:
2337 row.append( getattr( episode, name))
2338 self.model.append( row)
2340 for label in self.selection_buttons:
2341 button = gtk.Button( label)
2342 button.connect( 'clicked', self.custom_selection_button_clicked)
2343 self.hboxButtons.pack_start( button, expand = False)
2344 button.show_all()
2346 self.treeviewEpisodes.set_rules_hint( True)
2347 self.treeviewEpisodes.set_model( self.model)
2348 self.treeviewEpisodes.columns_autosize()
2349 self.calculate_total_size()
2351 def calculate_total_size( self):
2352 gl = gPodderLib()
2353 if self.size_attribute is not None:
2354 total_size = 0
2355 for index, row in enumerate( self.model):
2356 if self.model.get_value( row.iter, self.COLUMN_TOGGLE) == True:
2357 try:
2358 total_size += int(getattr( self.episodes[index], self.size_attribute))
2359 except:
2360 log( 'Cannot get size for %s', self.episodes[index].title, sender = self)
2362 if total_size > 0:
2363 self.labelTotalSize.set_text( _('Total size: %s') % gl.format_filesize( total_size))
2364 else:
2365 self.labelTotalSize.set_text( '')
2366 self.labelTotalSize.show_all()
2367 else:
2368 self.labelTotalSize.hide_all()
2370 def toggle_cell_handler( self, cell, path):
2371 model = self.treeviewEpisodes.get_model()
2372 model[path][self.COLUMN_TOGGLE] = not model[path][self.COLUMN_TOGGLE]
2374 if self.size_attribute is not None:
2375 self.calculate_total_size()
2377 def custom_selection_button_clicked( self, button):
2378 label = button.get_label()
2379 callback = self.selection_buttons[label]
2381 for index, row in enumerate( self.model):
2382 new_value = callback( self.episodes[index])
2383 self.model.set_value( row.iter, self.COLUMN_TOGGLE, new_value)
2385 self.calculate_total_size()
2387 def on_btnCheckAll_clicked( self, widget):
2388 for row in self.model:
2389 self.model.set_value( row.iter, self.COLUMN_TOGGLE, True)
2391 self.calculate_total_size()
2393 def on_btnCheckNone_clicked( self, widget):
2394 for row in self.model:
2395 self.model.set_value( row.iter, self.COLUMN_TOGGLE, False)
2397 self.calculate_total_size()
2399 def get_selected_episodes( self):
2400 selected_episodes = []
2402 for index, row in enumerate( self.model):
2403 if self.model.get_value( row.iter, self.COLUMN_TOGGLE) == True:
2404 selected_episodes.append( self.episodes[index])
2406 return selected_episodes
2408 def on_btnOK_clicked( self, widget):
2409 self.gPodderEpisodeSelector.destroy()
2410 if self.callback is not None:
2411 self.callback( self.get_selected_episodes())
2413 def on_btnCancel_clicked( self, widget):
2414 self.gPodderEpisodeSelector.destroy()
2415 if self.callback is not None:
2416 self.callback([])
2418 class gPodderConfigEditor(GladeWidget):
2419 def new(self):
2420 name_column = gtk.TreeViewColumn(_('Variable'))
2421 name_renderer = gtk.CellRendererText()
2422 name_column.pack_start(name_renderer)
2423 name_column.add_attribute(name_renderer, 'text', 0)
2424 name_column.add_attribute(name_renderer, 'weight', 5)
2425 self.configeditor.append_column(name_column)
2427 type_column = gtk.TreeViewColumn(_('Type'))
2428 type_renderer = gtk.CellRendererText()
2429 type_column.pack_start(type_renderer)
2430 type_column.add_attribute(type_renderer, 'text', 1)
2431 type_column.add_attribute(type_renderer, 'weight', 5)
2432 self.configeditor.append_column(type_column)
2434 value_column = gtk.TreeViewColumn(_('Value'))
2435 value_renderer = gtk.CellRendererText()
2436 value_column.pack_start(value_renderer)
2437 value_column.add_attribute(value_renderer, 'text', 2)
2438 value_column.add_attribute(value_renderer, 'editable', 4)
2439 value_column.add_attribute(value_renderer, 'weight', 5)
2440 value_renderer.connect('edited', self.value_edited)
2441 self.configeditor.append_column(value_column)
2443 gl = gPodderLib()
2444 self.model = gl.config.model()
2445 self.filter = self.model.filter_new()
2446 self.filter.set_visible_func(self.visible_func)
2448 self.configeditor.set_model(self.filter)
2449 self.configeditor.set_rules_hint(True)
2451 def visible_func(self, model, iter, user_data=None):
2452 text = self.entryFilter.get_text().lower()
2453 if text == '':
2454 return True
2455 else:
2456 # either the variable name or its value
2457 return (text in model.get_value(iter, 0).lower() or
2458 text in model.get_value(iter, 2).lower())
2460 def value_edited(self, renderer, path, new_text):
2461 gl = gPodderLib()
2463 model = self.configeditor.get_model()
2464 iter = model.get_iter(path)
2465 name = model.get_value(iter, 0)
2466 type_cute = model.get_value(iter, 1)
2468 if not gl.config.update_field(name, new_text):
2469 self.notification(_('Cannot set value of <b>%s</b> to <i>%s</i>.\n\nNeeded data type: %s') % (saxutils.escape(name), saxutils.escape(new_text), saxutils.escape(type_cute)), _('Error updating %s') % saxutils.escape(name))
2471 def on_entryFilter_changed(self, widget):
2472 self.filter.refilter()
2474 def on_btnShowAll_clicked(self, widget):
2475 self.entryFilter.set_text('')
2476 self.entryFilter.grab_focus()
2478 def on_configeditor_row_activated(self, treeview, path, view_column):
2479 model = treeview.get_model()
2480 it = model.get_iter(path)
2481 field_name = model.get_value(it, 0)
2482 field_type = model.get_value(it, 3)
2484 # Flip the boolean config flag
2485 if field_type == bool:
2486 gl = gPodderLib()
2487 gl.config.toggle_flag(field_name)
2489 def on_btnClose_clicked(self, widget):
2490 self.gPodderConfigEditor.destroy()
2493 def main():
2494 gobject.threads_init()
2495 gtk.window_set_default_icon_name( 'gpodder')
2497 gPodder().run()