Sun, 06 Apr 2008 17:22:11 +0200 <thp@perli.net>
[gpodder.git] / src / gpodder / gui.py
blobe65e9cdae0ef4e93e99f910df338a942f05a93a8
1 # -*- coding: utf-8 -*-
3 # gPodder - A media aggregator and podcast client
4 # Copyright (c) 2005-2008 Thomas Perl and the gPodder Team
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 subprocess
28 import glob
29 import time
31 from xml.sax import saxutils
33 from threading import Event
34 from threading import Thread
35 from string import strip
37 import gpodder
38 from gpodder import util
39 from gpodder import opml
40 from gpodder import services
41 from gpodder import sync
42 from gpodder import download
43 from gpodder import SimpleGladeApp
44 from gpodder.liblogger import log
46 try:
47 from gpodder import trayicon
48 have_trayicon = True
49 except Exception, exc:
50 log('Warning: Could not import gpodder.trayicon.')
51 log('Warning: This probably means your PyGTK installation is too old!')
52 have_trayicon = False
54 from libpodcasts import podcastChannel
55 from libpodcasts import channels_to_model
56 from libpodcasts import load_channels
57 from libpodcasts import save_channels
59 from gpodder.libgpodder import gl
61 from libplayers import UserAppsReader
63 from libtagupdate import tagging_supported
65 if gpodder.interface == gpodder.GUI:
66 WEB_BROWSER_ICON = 'web-browser'
67 elif gpodder.interface == gpodder.MAEMO:
68 import hildon
69 WEB_BROWSER_ICON = 'qgn_toolb_browser_web'
71 app_name = "gpodder"
72 app_version = "unknown" # will be set in main() call
73 app_authors = [
74 _('Current maintainer:'), 'Thomas Perl <thpinfo.com>',
75 '',
76 _('Patches, bug reports and donations by:'), 'Adrien Beaucreux',
77 'Alain Tauch', 'Alistair Sutton', 'Anders Kvist', 'Andy Busch',
78 'Antonio Roversi', 'Aravind Seshadri', 'Atte André Jensen',
79 'Bernd Schlapsi', 'Bill Barnard', 'Bjørn Rasmussen', 'Camille Moncelier',
80 'Carlos Moffat', 'Chris', 'Chris Arnold', 'Clark Burbidge', 'FFranci72',
81 'Florian Richter', 'FriedBunny', 'Gerrit Sangel', 'Götz Waschk',
82 'Haim Roitgrund', 'Hex', 'Holger Bauer', 'Holger Leskien', 'Jens Thiele',
83 'Jérôme Chabod', 'Jessica Henline', 'Joel Calado', 'John Ferguson',
84 'José Luis Fustel', 'Joseph Bleau', 'Julio Acuña',
85 'Konstantin Ryabitsev', 'Leonid Ponomarev', 'Mark Alford', 'Michael Salim',
86 'Mika Leppinen', 'Mike Coulson', 'Mykola Nikishov', 'narf at inode.at',
87 'Nick L.', 'Nicolas Quienot', 'Ondrej Vesely',
88 'Ortwin Forster', 'Paul Elliot', 'Paul Rudkin',
89 'Pavel Mlčoch', 'Peter Hoffmann', 'Pieter de Decker',
90 'Preben Randhol', 'Rafael Proença', 'red26wings', 'Richard Voigt',
91 'Robert Young', 'Roel Groeneveld', 'Seth Remington', 'Shane Donohoe',
92 'Stephan Buys', 'Stylianos Papanastasiou', 'Teo Ramirez',
93 'Thomas Matthijs', 'Thomas Mills Hinkle', 'Thomas Nilsson',
94 'Tim Michelsen', 'Tim Preetz', 'Todd Zullinger', 'VladDrac',
95 'Vladimir Zemlyakov', 'Wilfred van Rooijen',
96 '',
97 'List may be incomplete - please contact me.'
99 app_copyright = '© 2005-2008 Thomas Perl and the gPodder Team'
100 app_website = 'http://www.gpodder.org/'
102 # these will be filled with pathnames in bin/gpodder
103 glade_dir = [ 'share', 'gpodder' ]
104 icon_dir = [ 'share', 'pixmaps', 'gpodder.png' ]
105 scalable_dir = [ 'share', 'icons', 'hicolor', 'scalable', 'apps', 'gpodder.svg' ]
108 class GladeWidget(SimpleGladeApp.SimpleGladeApp):
109 gpodder_main_window = None
111 def __init__( self, **kwargs):
112 path = os.path.join( glade_dir, '%s.glade' % app_name)
113 root = self.__class__.__name__
114 domain = app_name
116 SimpleGladeApp.SimpleGladeApp.__init__( self, path, root, domain, **kwargs)
118 if root == 'gPodder':
119 GladeWidget.gpodder_main_window = self.gPodder
120 else:
121 # If we have a child window, set it transient for our main window
122 getattr( self, root).set_transient_for( GladeWidget.gpodder_main_window)
124 if gpodder.interface == gpodder.GUI:
125 if hasattr( self, 'center_on_widget'):
126 ( x, y ) = self.gpodder_main_window.get_position()
127 a = self.center_on_widget.allocation
128 ( x, y ) = ( x + a.x, y + a.y )
129 ( w, h ) = ( a.width, a.height )
130 ( pw, ph ) = getattr( self, root).get_size()
131 getattr( self, root).move( x + w/2 - pw/2, y + h/2 - ph/2)
132 else:
133 getattr( self, root).set_position( gtk.WIN_POS_CENTER_ON_PARENT)
135 def notification(self, message, title=None):
136 util.idle_add(self.show_message, message, title)
138 def show_message( self, message, title = None):
139 if hasattr(self, 'tray_icon') and hasattr(self, 'minimized') and self.tray_icon and self.minimized:
140 if title is None:
141 title = 'gPodder'
142 self.tray_icon.send_notification(message, title)
143 return
145 if gpodder.interface == gpodder.GUI:
146 dlg = gtk.MessageDialog(GladeWidget.gpodder_main_window, gtk.DIALOG_MODAL, gtk.MESSAGE_INFO, gtk.BUTTONS_OK)
147 if title:
148 dlg.set_title(str(title))
149 dlg.set_markup('<span weight="bold" size="larger">%s</span>\n\n%s' % (title, message))
150 else:
151 dlg.set_markup('<span weight="bold" size="larger">%s</span>' % (message))
152 elif gpodder.interface == gpodder.MAEMO:
153 dlg = hildon.Note('information', (GladeWidget.gpodder_main_window, message))
155 dlg.run()
156 dlg.destroy()
158 def show_confirmation( self, message, title = None):
159 if gpodder.interface == gpodder.GUI:
160 affirmative = gtk.RESPONSE_YES
161 dlg = gtk.MessageDialog(GladeWidget.gpodder_main_window, gtk.DIALOG_MODAL, gtk.MESSAGE_QUESTION, gtk.BUTTONS_YES_NO)
162 if title:
163 dlg.set_title(str(title))
164 dlg.set_markup('<span weight="bold" size="larger">%s</span>\n\n%s' % (title, message))
165 else:
166 dlg.set_markup('<span weight="bold" size="larger">%s</span>' % (message))
167 elif gpodder.interface == gpodder.MAEMO:
168 affirmative = gtk.RESPONSE_OK
169 dlg = hildon.Note('confirmation', (GladeWidget.gpodder_main_window, message))
171 response = dlg.run()
172 dlg.destroy()
174 return response == affirmative
176 def show_copy_dialog( self, src_filename, dst_filename = None, dst_directory = None, title = _('Select destination')):
177 if dst_filename is None:
178 dst_filename = src_filename
180 if dst_directory is None:
181 dst_directory = os.path.expanduser( '~')
183 ( base, extension ) = os.path.splitext( src_filename)
185 if not dst_filename.endswith( extension):
186 dst_filename += extension
188 dlg = gtk.FileChooserDialog( title = title, parent = GladeWidget.gpodder_main_window, action = gtk.FILE_CHOOSER_ACTION_SAVE)
189 dlg.set_do_overwrite_confirmation( True)
191 dlg.set_current_name( os.path.basename( dst_filename))
192 dlg.set_current_folder( dst_directory)
194 dlg.add_button( gtk.STOCK_CANCEL, gtk.RESPONSE_CANCEL)
195 dlg.add_button( gtk.STOCK_SAVE, gtk.RESPONSE_OK)
197 if dlg.run() == gtk.RESPONSE_OK:
198 dst_filename = dlg.get_filename()
199 if not dst_filename.endswith( extension):
200 dst_filename += extension
202 log( 'Copying %s => %s', src_filename, dst_filename, sender = self)
204 try:
205 shutil.copyfile( src_filename, dst_filename)
206 except:
207 log( 'Error copying file.', sender = self, traceback = True)
209 dlg.destroy()
213 class gPodder(GladeWidget):
214 def new(self):
215 if gpodder.interface == gpodder.MAEMO:
216 # Maemo-specific changes to the UI
217 global scalable_dir
218 scalable_dir = scalable_dir.replace('.svg', '.png')
220 self.app = hildon.Program()
221 gtk.set_application_name('gPodder')
222 self.window = hildon.Window()
223 self.window.connect('delete-event', self.on_gPodder_delete_event)
224 self.window.connect('window-state-event', self.window_state_event)
225 self.window.connect('key-press-event', self.on_key_press)
227 # Give toolbar to the hildon window
228 self.toolbar.parent.remove(self.toolbar)
229 self.toolbar.set_style(gtk.TOOLBAR_ICONS)
230 self.window.add_toolbar(self.toolbar)
232 self.app.add_window(self.window)
233 self.vMain.reparent(self.window)
234 self.gPodder.destroy()
235 self.gPodder = self.window
237 self.set_title(_('Podcasts'))
239 # Reparent the main menu
240 menu = gtk.Menu()
241 for child in self.mainMenu.get_children():
242 child.reparent(menu)
243 self.itemClose.reparent(menu)
244 self.trennlinie3.parent.remove(self.trennlinie3)
245 self.window.set_menu(menu)
247 self.mainMenu.destroy()
248 self.window.show_all()
250 # do some widget hiding
251 self.toolbar.remove(self.toolTransfer)
252 self.itemTransferSelected.hide_all()
253 self.separator11.hide_all()
254 self.label120.set_text(_('Update feeds'))
255 self.label120.set_padding(0, 10)
257 self.uar = None
258 self.tray_icon = None
260 self.fullscreen = False
261 self.minimized = False
262 self.gPodder.connect('window-state-event', self.window_state_event)
264 self.show_hide_tray_icon()
265 self.already_notified_new_episodes = []
267 self.episode_description_shown = gl.config.episode_list_descriptions
268 self.itemShowToolbar.set_active(gl.config.show_toolbar)
269 self.itemShowDescription.set_active(gl.config.episode_list_descriptions)
270 self.update_view_settings()
272 if self.tray_icon:
273 if gl.config.start_iconified:
274 self.iconify_main_window()
275 elif gl.config.minimize_to_tray:
276 self.tray_icon.set_visible(False)
278 gl.config.connect_gtk_window( self.gPodder)
279 gl.config.connect_gtk_paned( 'paned_position', self.channelPaned)
281 while gtk.events_pending():
282 gtk.main_iteration( False)
284 self.default_title = None
285 if app_version.rfind('svn') != -1:
286 self.set_title('gPodder %s' % app_version)
287 else:
288 self.set_title(self.gPodder.get_title())
290 gtk.about_dialog_set_url_hook(lambda dlg, link, data: util.open_website(link), None)
292 # cell renderers for channel tree
293 namecolumn = gtk.TreeViewColumn( _('Channel'))
295 iconcell = gtk.CellRendererPixbuf()
296 namecolumn.pack_start( iconcell, False)
297 namecolumn.add_attribute( iconcell, 'pixbuf', 5)
299 namecell = gtk.CellRendererText()
300 namecell.set_property('ellipsize', pango.ELLIPSIZE_END)
301 namecolumn.pack_start( namecell, True)
302 namecolumn.add_attribute( namecell, 'markup', 2)
303 namecolumn.add_attribute( namecell, 'weight', 4)
305 iconcell = gtk.CellRendererPixbuf()
306 namecolumn.pack_start( iconcell, False)
307 namecolumn.add_attribute( iconcell, 'pixbuf', 3)
309 self.treeChannels.append_column( namecolumn)
310 self.treeChannels.set_headers_visible(False)
312 # enable alternating colors hint
313 self.treeAvailable.set_rules_hint( True)
314 self.treeChannels.set_rules_hint( True)
316 # connect to tooltip signals
317 try:
318 self.treeChannels.set_property('has-tooltip', True)
319 self.treeChannels.connect('query-tooltip', self.treeview_channels_query_tooltip)
320 except:
321 log('No tooltips for channel navigator (need at least PyGTK 2.12)', sender = self)
322 self.last_tooltip_channel = None
324 # Add our context menu to treeAvailable
325 if gpodder.interface == gpodder.MAEMO:
326 self.treeAvailable.connect('button-release-event', self.treeview_button_pressed)
327 else:
328 self.treeAvailable.connect('button-press-event', self.treeview_button_pressed)
329 self.treeChannels.connect('button-press-event', self.treeview_channels_button_pressed)
331 iconcell = gtk.CellRendererPixbuf()
332 if gpodder.interface == gpodder.MAEMO:
333 status_column_label = ''
334 else:
335 status_column_label = _('Status')
336 iconcolumn = gtk.TreeViewColumn(status_column_label, iconcell, pixbuf=4)
338 namecell = gtk.CellRendererText()
339 namecell.set_property('ellipsize', pango.ELLIPSIZE_END)
340 namecolumn = gtk.TreeViewColumn(_("Episode"), namecell, markup=6)
341 namecolumn.set_sizing(gtk.TREE_VIEW_COLUMN_AUTOSIZE)
342 namecolumn.set_expand(True)
344 sizecell = gtk.CellRendererText()
345 sizecolumn = gtk.TreeViewColumn( _("Size"), sizecell, text=2)
347 releasecell = gtk.CellRendererText()
348 releasecolumn = gtk.TreeViewColumn( _("Released"), releasecell, text=5)
350 for itemcolumn in (iconcolumn, namecolumn, sizecolumn, releasecolumn):
351 itemcolumn.set_reorderable(True)
352 self.treeAvailable.append_column(itemcolumn)
354 if gpodder.interface == gpodder.MAEMO:
355 # Due to screen space contraints, we
356 # hide these columns here by default
357 self.column_size = sizecolumn
358 self.column_released = releasecolumn
359 self.column_released.set_visible(False)
360 self.column_size.set_visible(False)
362 # enable search in treeavailable
363 self.treeAvailable.set_search_equal_func( self.treeAvailable_search_equal)
365 # enable multiple selection support
366 self.treeAvailable.get_selection().set_mode( gtk.SELECTION_MULTIPLE)
367 self.treeDownloads.get_selection().set_mode( gtk.SELECTION_MULTIPLE)
369 # columns and renderers for "download progress" tab
370 episodecell = gtk.CellRendererText()
371 episodecell.set_property('ellipsize', pango.ELLIPSIZE_END)
372 episodecolumn = gtk.TreeViewColumn( _("Episode"), episodecell, text=0)
373 episodecolumn.set_sizing(gtk.TREE_VIEW_COLUMN_AUTOSIZE)
374 episodecolumn.set_expand(True)
376 speedcell = gtk.CellRendererText()
377 speedcolumn = gtk.TreeViewColumn( _("Speed"), speedcell, text=1)
379 progresscell = gtk.CellRendererProgress()
380 progresscolumn = gtk.TreeViewColumn( _("Progress"), progresscell, value=2)
381 progresscolumn.set_expand(True)
383 for itemcolumn in ( episodecolumn, speedcolumn, progresscolumn ):
384 self.treeDownloads.append_column( itemcolumn)
386 services.download_status_manager.register( 'list-changed', self.download_status_updated)
387 services.download_status_manager.register( 'progress-changed', self.download_progress_updated)
389 self.treeDownloads.set_model( services.download_status_manager.tree_model)
391 #Add Drag and Drop Support
392 flags = gtk.DEST_DEFAULT_ALL
393 targets = [ ('text/plain', 0, 2), ('STRING', 0, 3), ('TEXT', 0, 4) ]
394 actions = gtk.gdk.ACTION_DEFAULT | gtk.gdk.ACTION_COPY
395 self.treeChannels.drag_dest_set( flags, targets, actions)
396 self.treeChannels.connect( 'drag_data_received', self.drag_data_received)
398 # Subscribed channels
399 self.active_channel = None
400 self.channels = load_channels( load_items = False, offline = True)
402 # load list of user applications for audio playback
403 self.user_apps_reader = UserAppsReader(['audio', 'video'])
404 Thread(target=self.read_apps).start()
406 # Clean up old, orphaned download files
407 gl.clean_up_downloads( delete_partial = True)
409 # Set the "Device" menu item for the first time
410 self.update_item_device()
412 # Now, update the feed cache, when everything's in place
413 self.update_feed_cache(force_update=gl.config.update_on_startup)
415 # Start the auto-update procedure
416 self.auto_update_procedure(first_run=True)
418 # Delete old episodes if the user wishes to
419 if gl.config.auto_remove_old_episodes:
420 old_episodes = self.get_old_episodes()
421 if len(old_episodes) > 0:
422 self.delete_episode_list(old_episodes, confirm=False)
423 self.updateComboBox()
425 def read_apps(self):
426 time.sleep(3) # give other parts of gpodder a chance to start up
427 self.user_apps_reader.read()
428 util.idle_add(self.user_apps_reader.get_applications_as_model, 'audio', False)
429 util.idle_add(self.user_apps_reader.get_applications_as_model, 'video', False)
431 def treeview_channels_query_tooltip(self, treeview, x, y, keyboard_tooltip, tooltip):
432 (path, column, rx, ry) = treeview.get_path_at_pos( x, y) or (None,)*4
434 if path is not None:
435 model = treeview.get_model()
436 iter = model.get_iter(path)
437 url = model.get_value(iter, 0)
438 for channel in self.channels:
439 if channel.url == url:
440 if self.last_tooltip_channel is not None and self.last_tooltip_channel != channel:
441 self.last_tooltip_channel = None
442 return False
443 self.last_tooltip_channel = channel
444 tooltip.set_icon(channel.get_cover_pixbuf())
445 diskspace_str = _('Used disk space: %s') % util.format_filesize(channel.save_dir_size)
446 error_str = model.get_value(iter, 6)
447 if error_str:
448 error_str = _('Feedparser error: %s') % saxutils.escape(error_str.strip())
449 error_str = '<span foreground="#ff0000">%s</span>\n' % error_str
450 tooltip.set_markup( '<b>%s</b>\n<small><i>%s</i></small>\n%s%s\n\n<small>%s</small>' % (saxutils.escape(channel.title), saxutils.escape(channel.url), error_str, saxutils.escape(channel.description), diskspace_str))
451 return True
453 self.last_tooltip_channel = None
454 return False
456 def update_m3u_playlist_clicked(self, widget):
457 self.active_channel.update_m3u_playlist()
458 self.show_message(_('Updated M3U playlist in download folder.'), _('Updated playlist'))
460 def treeview_channels_button_pressed( self, treeview, event):
461 global WEB_BROWSER_ICON
463 if event.button == 3:
464 ( x, y ) = ( int(event.x), int(event.y) )
465 ( path, column, rx, ry ) = treeview.get_path_at_pos( x, y) or (None,)*4
467 paths = []
469 # Did the user right-click into a selection?
470 selection = treeview.get_selection()
471 if selection.count_selected_rows() and path:
472 ( model, paths ) = selection.get_selected_rows()
473 if path not in paths:
474 # We have right-clicked, but not into the
475 # selection, assume we don't want to operate
476 # on the selection
477 paths = []
479 # No selection or right click not in selection:
480 # Select the single item where we clicked
481 if not len( paths) and path:
482 treeview.grab_focus()
483 treeview.set_cursor( path, column, 0)
485 ( model, paths ) = ( treeview.get_model(), [ path ] )
487 # We did not find a selection, and the user didn't
488 # click on an item to select -- don't show the menu
489 if not len( paths):
490 return True
492 menu = gtk.Menu()
494 item = gtk.ImageMenuItem( _('Open download folder'))
495 item.set_image( gtk.image_new_from_icon_name( 'folder-open', gtk.ICON_SIZE_MENU))
496 item.connect('activate', lambda x: util.gui_open(self.active_channel.save_dir))
497 menu.append( item)
499 if gl.config.create_m3u_playlists:
500 item = gtk.ImageMenuItem(_('Update M3U playlist'))
501 item.set_image(gtk.image_new_from_stock(gtk.STOCK_REFRESH, gtk.ICON_SIZE_MENU))
502 item.connect('activate', self.update_m3u_playlist_clicked)
503 menu.append(item)
505 if self.active_channel.link:
506 item = gtk.ImageMenuItem(_('Visit website'))
507 item.set_image(gtk.image_new_from_icon_name(WEB_BROWSER_ICON, gtk.ICON_SIZE_MENU))
508 item.connect('activate', lambda w: util.open_website(self.active_channel.link))
509 menu.append(item)
511 menu.append( gtk.SeparatorMenuItem())
513 item = gtk.ImageMenuItem(gtk.STOCK_EDIT)
514 item.connect( 'activate', self.on_itemEditChannel_activate)
515 menu.append( item)
517 item = gtk.ImageMenuItem(gtk.STOCK_DELETE)
518 item.connect( 'activate', self.on_itemRemoveChannel_activate)
519 menu.append( item)
521 menu.show_all()
522 menu.popup( None, None, None, event.button, event.time)
524 return True
526 def save_episode_as_file( self, url, *args):
527 episode = self.active_channel.find_episode( url)
529 self.show_copy_dialog( src_filename = episode.local_filename(), dst_filename = episode.sync_filename())
531 def copy_episode_bluetooth(self, url, *args):
532 episode = self.active_channel.find_episode(url)
533 filename = episode.local_filename()
535 if gl.config.bluetooth_ask_always:
536 device = None
537 else:
538 device = gl.config.bluetooth_device_address
540 destfile = os.path.join(gl.tempdir, episode.sync_filename())
541 (base, ext) = os.path.splitext(filename)
542 if not destfile.endswith(ext):
543 destfile += ext
545 if gl.config.bluetooth_use_converter:
546 title = _('Converting file')
547 message = _('Please wait while gPodder converts your media file for bluetooth file transfer.')
548 dlg = gtk.MessageDialog(self.gPodder, gtk.DIALOG_MODAL, gtk.MESSAGE_INFO, gtk.BUTTONS_NONE)
549 dlg.set_title(title)
550 dlg.set_markup('<span weight="bold" size="larger">%s</span>\n\n%s'%(title, message))
551 dlg.show_all()
552 else:
553 dlg = None
555 def convert_and_send_thread(filename, destfile, device, dialog, notify):
556 if gl.config.bluetooth_use_converter:
557 p = subprocess.Popen([gl.config.bluetooth_converter, filename, destfile], stdout=sys.stdout, stderr=sys.stderr)
558 result = p.wait()
559 if dialog is not None:
560 dialog.destroy()
561 else:
562 try:
563 shutil.copyfile(filename, destfile)
564 result = 0
565 except:
566 log('Cannot copy "%s" to "%s".', filename, destfile, sender=self)
567 result = 1
569 if result == 0 or not os.path.exists(destfile):
570 util.bluetooth_send_file(destfile, device)
571 else:
572 notify(_('Error converting file.'), _('Bluetooth file transfer'))
573 util.delete_file(destfile)
575 Thread(target=convert_and_send_thread, args=[filename, destfile, device, dlg, self.notification]).start()
577 def treeview_button_pressed( self, treeview, event):
578 global WEB_BROWSER_ICON
580 # Use right-click for the Desktop version and left-click for Maemo
581 if (event.button == 1 and gpodder.interface == gpodder.MAEMO) or \
582 (event.button == 3 and gpodder.interface == gpodder.GUI):
583 ( x, y ) = ( int(event.x), int(event.y) )
584 ( path, column, rx, ry ) = treeview.get_path_at_pos( x, y) or (None,)*4
586 paths = []
588 # Did the user right-click into a selection?
589 selection = self.treeAvailable.get_selection()
590 if selection.count_selected_rows() and path:
591 ( model, paths ) = selection.get_selected_rows()
592 if path not in paths:
593 # We have right-clicked, but not into the
594 # selection, assume we don't want to operate
595 # on the selection
596 paths = []
598 # No selection or right click not in selection:
599 # Select the single item where we clicked
600 if not len( paths) and path:
601 treeview.grab_focus()
602 treeview.set_cursor( path, column, 0)
604 ( model, paths ) = ( treeview.get_model(), [ path ] )
606 # We did not find a selection, and the user didn't
607 # click on an item to select -- don't show the menu
608 if not len( paths):
609 return True
611 first_url = model.get_value( model.get_iter( paths[0]), 0)
613 menu = gtk.Menu()
615 ( can_play, can_download, can_transfer, can_cancel ) = self.play_or_download()
617 if can_play:
618 item = gtk.ImageMenuItem(gtk.STOCK_MEDIA_PLAY)
619 item.connect( 'activate', lambda w: self.on_treeAvailable_row_activated( self.toolPlay))
620 menu.append( item)
622 is_locked = gl.history_is_locked(first_url)
623 if not is_locked:
624 item = gtk.ImageMenuItem(gtk.STOCK_DELETE)
625 item.connect('activate', self.on_btnDownloadedDelete_clicked)
626 menu.append(item)
628 if can_cancel:
629 item = gtk.ImageMenuItem( _('Cancel download'))
630 item.set_image( gtk.image_new_from_stock( gtk.STOCK_STOP, gtk.ICON_SIZE_MENU))
631 item.connect( 'activate', lambda w: self.on_treeDownloads_row_activated( self.toolCancel))
632 menu.append( item)
634 if can_download:
635 item = gtk.ImageMenuItem(_('Download'))
636 item.set_image( gtk.image_new_from_stock( gtk.STOCK_GO_DOWN, gtk.ICON_SIZE_MENU))
637 item.connect( 'activate', lambda w: self.on_treeAvailable_row_activated( self.toolDownload))
638 menu.append( item)
640 is_downloaded = gl.history_is_downloaded(first_url)
641 if is_downloaded:
642 item = gtk.ImageMenuItem(_('Mark as not downloaded'))
643 item.set_image( gtk.image_new_from_stock( gtk.STOCK_UNDELETE, gtk.ICON_SIZE_MENU))
644 item.connect( 'activate', lambda w: self.on_item_toggle_downloaded_activate( w, False, False))
645 menu.append( item)
646 else:
647 # FIXME: foreach episode go and delete/toggle downloaded episode
648 # ++ unify into a single menu item (with "Delete" from above)
649 item = gtk.ImageMenuItem(gtk.STOCK_DELETE)
650 item.connect( 'activate', lambda w: self.on_item_toggle_downloaded_activate( w, False, True))
651 menu.append( item)
653 if can_play:
654 menu.append( gtk.SeparatorMenuItem())
655 item = gtk.ImageMenuItem(_('Save to disk'))
656 item.set_image(gtk.image_new_from_stock(gtk.STOCK_SAVE_AS, gtk.ICON_SIZE_MENU))
657 item.connect( 'activate', lambda w: self.save_episode_as_file( episode_url))
658 menu.append( item)
659 if gl.config.bluetooth_enabled:
660 item = gtk.ImageMenuItem(_('Send via bluetooth'))
661 item.set_image(gtk.image_new_from_icon_name('bluetooth', gtk.ICON_SIZE_MENU))
662 item.connect('activate', lambda w: self.copy_episode_bluetooth(episode_url))
663 menu.append( item)
664 if can_transfer:
665 item = gtk.ImageMenuItem(_('Transfer to %s') % gl.get_device_name())
666 item.set_image(gtk.image_new_from_icon_name('multimedia-player', gtk.ICON_SIZE_MENU))
667 item.connect('activate', lambda w: self.on_treeAvailable_row_activated(self.toolTransfer))
668 menu.append(item)
670 if can_play:
671 menu.append( gtk.SeparatorMenuItem())
672 is_played = gl.history_is_played(first_url)
673 if is_played:
674 item = gtk.ImageMenuItem(_('Mark as unplayed'))
675 item.set_image( gtk.image_new_from_stock( gtk.STOCK_CANCEL, gtk.ICON_SIZE_MENU))
676 item.connect( 'activate', lambda w: self.on_item_toggle_played_activate( w, False, False))
677 menu.append( item)
678 else:
679 item = gtk.ImageMenuItem(_('Mark as played'))
680 item.set_image( gtk.image_new_from_stock( gtk.STOCK_APPLY, gtk.ICON_SIZE_MENU))
681 item.connect( 'activate', lambda w: self.on_item_toggle_played_activate( w, False, True))
682 menu.append( item)
684 is_locked = gl.history_is_locked(first_url)
685 if is_locked:
686 item = gtk.ImageMenuItem(_('Allow deletion'))
687 item.set_image(gtk.image_new_from_stock(gtk.STOCK_DIALOG_AUTHENTICATION, gtk.ICON_SIZE_MENU))
688 item.connect('activate', self.on_item_toggle_lock_activate)
689 menu.append(item)
690 else:
691 item = gtk.ImageMenuItem(_('Prohibit deletion'))
692 item.set_image(gtk.image_new_from_stock(gtk.STOCK_DIALOG_AUTHENTICATION, gtk.ICON_SIZE_MENU))
693 item.connect('activate', self.on_item_toggle_lock_activate)
694 menu.append(item)
696 if len(paths) == 1:
697 menu.append(gtk.SeparatorMenuItem())
698 # Single item, add episode information menu item
699 episode_url = model.get_value( model.get_iter( paths[0]), 0)
700 item = gtk.ImageMenuItem(_('Episode details'))
701 item.set_image( gtk.image_new_from_stock( gtk.STOCK_INFO, gtk.ICON_SIZE_MENU))
702 item.connect( 'activate', lambda w: self.on_treeAvailable_row_activated( self.treeAvailable))
703 menu.append( item)
704 episode = self.active_channel.find_episode(episode_url)
705 # If we have it, also add episode website link
706 if episode and episode.link and episode.link != episode.url:
707 item = gtk.ImageMenuItem(_('Visit website'))
708 item.set_image(gtk.image_new_from_icon_name(WEB_BROWSER_ICON, gtk.ICON_SIZE_MENU))
709 item.connect('activate', lambda w: util.open_website(episode.link))
710 menu.append(item)
712 if gpodder.interface == gpodder.MAEMO:
713 # Because we open the popup on left-click for Maemo,
714 # we also include a non-action to close the menu
715 menu.append(gtk.SeparatorMenuItem())
716 item = gtk.ImageMenuItem(_('Close this menu'))
717 item.set_image(gtk.image_new_from_stock(gtk.STOCK_CLOSE, gtk.ICON_SIZE_MENU))
718 menu.append(item)
720 menu.show_all()
721 menu.popup( None, None, None, event.button, event.time)
723 return True
725 def set_title(self, new_title):
726 self.default_title = new_title
727 self.gPodder.set_title(new_title)
729 def download_progress_updated( self, count, percentage):
730 title = [ self.default_title ]
732 if count == 1:
733 title.append( _('downloading one file'))
734 elif count > 1:
735 title.append( _('downloading %d files') % count)
737 if len(title) == 2:
738 title[1] = ''.join( [ title[1], ' (%d%%)' % ( percentage, ) ])
740 self.gPodder.set_title( ' - '.join( title))
742 def playback_episode( self, current_channel, current_podcast):
743 (success, application) = gl.playback_episode(current_channel, current_podcast)
744 if not success:
745 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), ))
746 self.download_status_updated()
748 def treeAvailable_search_equal( self, model, column, key, iter, data = None):
749 if model is None:
750 return True
752 key = key.lower()
754 # columns, as defined in libpodcasts' get model method
755 # 1 = episode title, 7 = description
756 columns = (1, 7)
758 for column in columns:
759 value = model.get_value( iter, column).lower()
760 if value.find( key) != -1:
761 return False
763 return True
765 def play_or_download( self):
766 if self.wNotebook.get_current_page() > 0:
767 return
769 ( can_play, can_download, can_transfer, can_cancel ) = (False,)*4
771 selection = self.treeAvailable.get_selection()
772 if selection.count_selected_rows() > 0:
773 (model, paths) = selection.get_selected_rows()
775 for path in paths:
776 url = model.get_value( model.get_iter( path), 0)
777 local_filename = model.get_value( model.get_iter( path), 8)
779 if os.path.exists( local_filename):
780 can_play = True
781 else:
782 if services.download_status_manager.is_download_in_progress( url):
783 can_cancel = True
784 else:
785 can_download = True
787 if util.file_type_by_extension( util.file_extension_from_url( url)) == 'torrent':
788 can_download = can_download or gl.config.use_gnome_bittorrent
790 can_download = can_download and not can_cancel
791 can_play = can_play and not can_cancel and not can_download
792 can_transfer = can_play and gl.config.device_type != 'none'
794 self.toolPlay.set_sensitive( can_play)
795 self.toolDownload.set_sensitive( can_download)
796 self.toolTransfer.set_sensitive( can_transfer)
797 self.toolCancel.set_sensitive( can_cancel)
799 return ( can_play, can_download, can_transfer, can_cancel )
801 def download_status_updated( self):
802 count = services.download_status_manager.count()
803 if count:
804 self.labelDownloads.set_text( _('Downloads (%d)') % count)
805 else:
806 self.labelDownloads.set_text( _('Downloads'))
808 for channel in self.channels:
809 channel.update_model()
811 self.updateComboBox()
813 def updateComboBox( self):
814 ( model, iter ) = self.treeChannels.get_selection().get_selected()
816 if model and iter:
817 selected = model.get_path( iter)
818 else:
819 selected = (0,)
821 rect = self.treeChannels.get_visible_rect()
822 self.treeChannels.set_model(channels_to_model(self.channels))
823 self.treeChannels.scroll_to_point( rect.x, rect.y)
824 while gtk.events_pending():
825 gtk.main_iteration( False)
826 self.treeChannels.scroll_to_point( rect.x, rect.y)
828 try:
829 self.treeChannels.get_selection().select_path( selected)
830 except:
831 log( 'Cannot set selection on treeChannels', sender = self)
832 self.on_treeChannels_cursor_changed( self.treeChannels)
834 def updateTreeView( self):
835 if self.channels and self.active_channel is not None:
836 self.treeAvailable.set_model(self.active_channel.tree_model)
837 self.treeAvailable.columns_autosize()
838 self.play_or_download()
839 else:
840 if self.treeAvailable.get_model():
841 self.treeAvailable.get_model().clear()
843 def drag_data_received(self, widget, context, x, y, sel, ttype, time):
844 result = sel.data
845 self.add_new_channel( result)
847 def add_new_channel( self, result = None, ask_download_new = True):
848 result = util.normalize_feed_url( result)
850 if result:
851 for old_channel in self.channels:
852 if old_channel.url == result:
853 self.show_message( _('You have already subscribed to this channel: %s') % ( saxutils.escape( old_channel.title), ), _('Already added'))
854 log( 'Channel already exists: %s', result)
855 # Select the existing channel in combo box
856 for i in range( len( self.channels)):
857 if self.channels[i] == old_channel:
858 self.treeChannels.get_selection().select_path( (i,))
859 return
860 log( 'Adding new channel: %s', result)
861 try:
862 channel = podcastChannel.get_by_url( url = result, force_update = True)
863 except:
864 log('Error in podcastChannel.get_by_url(%s)', result, sender=self)
865 channel = None
867 if channel:
868 self.channels.append( channel)
869 save_channels( self.channels)
870 # download changed channels
871 self.update_feed_cache(force_update=False)
873 (username, password) = util.username_password_from_url( result)
874 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')):
875 channel.username = username
876 channel.password = password
877 log('Saving authentication data for episode downloads..', sender = self)
878 channel.save_settings()
880 # ask user to download some new episodes
881 self.treeChannels.get_selection().select_path( (len( self.channels)-1,))
882 self.active_channel = channel
883 self.updateTreeView()
884 if ask_download_new:
885 self.on_btnDownloadNewer_clicked( None)
886 else:
887 title = _('Error adding channel')
888 message = _('The channel could not be added. Please check the spelling of the URL or try again later.')
889 self.show_message( message, title)
890 else:
891 if result:
892 title = _('URL scheme not supported')
893 message = _('gPodder currently only supports URLs starting with <b>http://</b>, <b>feed://</b> or <b>ftp://</b>.')
894 self.show_message( message, title)
896 def update_feed_cache_callback(self, progressbar, position, count):
897 title = self.channels[position].title
898 progression = _('Updating %s (%d/%d)')%(title, position+1, count)
899 progressbar.set_text(progression)
900 if self.tray_icon:
901 self.tray_icon.set_status(self.tray_icon.STATUS_UPDATING_FEED_CACHE, progression)
903 if count > 0:
904 progressbar.set_fraction(float(position)/float(count))
906 def update_feed_cache_finish_callback(self, force_update=False, please_wait_dialog=None, notify_no_new_episodes=False):
907 if please_wait_dialog is not None:
908 please_wait_dialog.destroy()
910 self.updateComboBox()
912 if self.tray_icon:
913 self.tray_icon.set_status(None)
914 if self.minimized and force_update:
915 new_episodes = []
916 # look for new episodes to notify
917 for channel in self.channels:
918 for episode in channel.get_new_episodes():
919 if not episode.url in self.already_notified_new_episodes:
920 new_episodes.append(episode)
921 self.already_notified_new_episodes.append(episode.url)
922 # notify new episodes
924 if len(new_episodes) == 0:
925 if notify_no_new_episodes and self.tray_icon is not None:
926 msg = _('No new episodes available for download')
927 self.tray_icon.send_notification(msg)
928 return
929 elif len(new_episodes) == 1:
930 title = _('gPodder has found %s') % (_('one new episode:'),)
931 else:
932 title = _('gPodder has found %s') % (_('%i new episodes:') % len(new_episodes))
933 message = self.tray_icon.format_episode_list(new_episodes)
935 #auto download new episodes
936 if gl.config.auto_download_when_minimized:
937 message += '\n<i>(%s...)</i>' % _('downloading')
938 self.download_episode_list(new_episodes)
939 self.tray_icon.send_notification(message, title)
940 return
942 # open the episodes selection dialog
943 if force_update:
944 self.on_itemDownloadAllNew_activate( self.gPodder)
946 def update_feed_cache_proc( self, force_update, callback_proc = None, callback_error = None, finish_proc = None):
947 self.channels = load_channels( force_update = force_update, callback_proc = callback_proc, callback_error = callback_error, offline = not force_update)
948 if finish_proc:
949 finish_proc()
951 def update_feed_cache(self, force_update=True, notify_no_new_episodes=False):
952 if self.tray_icon:
953 self.tray_icon.set_status(self.tray_icon.STATUS_UPDATING_FEED_CACHE)
955 # skip dialog if the window is not active and tray icon is displayed
956 if self.minimized and self.tray_icon:
957 show_update_dialog = False
958 else:
959 show_update_dialog = True
961 please_wait = None
962 if show_update_dialog:
963 if force_update:
964 title = _('Downloading podcast feeds')
965 heading = _('Downloading feeds')
966 else:
967 title = _('Loading podcast feeds')
968 heading = _('Loading feeds')
969 body = _('Podcast feeds contain channel metadata and information about current episodes.')
971 please_wait = gtk.Dialog(title, self.gPodder, gtk.DIALOG_MODAL, (gtk.STOCK_CANCEL, gtk.RESPONSE_CANCEL))
972 please_wait.set_transient_for(self.gPodder)
973 please_wait.set_position(gtk.WIN_POS_CENTER_ON_PARENT)
974 please_wait.vbox.set_spacing(5)
975 please_wait.set_border_width(5)
976 please_wait.set_resizable(False)
978 label_heading = gtk.Label()
979 label_heading.set_alignment(0.0, 0.5)
980 label_heading.set_markup('<span weight="bold" size="larger">%s</span>'%heading)
982 label_body = gtk.Label(body)
983 label_body.set_alignment(0.0, 0.5)
984 label_body.set_line_wrap(True)
986 progressbar = gtk.ProgressBar()
987 progressbar.set_ellipsize(pango.ELLIPSIZE_MIDDLE)
989 # put it all together
990 for widget in (label_heading, label_body, progressbar):
991 please_wait.vbox.pack_start(widget)
992 please_wait.show_all()
994 # center the dialog on the gPodder main window
995 (x, y) = self.gPodder.get_position()
996 (w, h) = self.gPodder.get_size()
997 (pw, ph) = please_wait.get_size()
998 please_wait.move(int(x+w/2-pw/2), int(y+h/2-ph/2))
1000 # hide separator line
1001 please_wait.set_has_separator(False)
1003 # let's get down to business..
1004 if show_update_dialog:
1005 callback_proc = lambda pos, count: util.idle_add(self.update_feed_cache_callback, progressbar, pos, count)
1006 else:
1007 callback_proc = None
1008 finish_proc = lambda: util.idle_add(self.update_feed_cache_finish_callback, force_update, please_wait, notify_no_new_episodes)
1010 args = (force_update, callback_proc, self.notification, finish_proc)
1012 thread = Thread( target = self.update_feed_cache_proc, args = args)
1013 thread.start()
1015 if please_wait is not None:
1016 please_wait.run()
1017 please_wait.destroy()
1019 def download_podcast_by_url( self, url, want_message_dialog = True, widget = None):
1020 if self.active_channel is None:
1021 return
1023 current_channel = self.active_channel
1024 current_podcast = current_channel.find_episode( url)
1025 filename = current_podcast.local_filename()
1027 if widget:
1028 if (widget.get_name() == 'itemPlaySelected' or widget.get_name() == 'toolPlay') and os.path.exists( filename):
1029 # addDownloadedItem just to make sure the episode is marked correctly in localdb
1030 current_channel.addDownloadedItem( current_podcast)
1031 # open the file now
1032 if current_podcast.file_type() != 'torrent':
1033 self.playback_episode( current_channel, current_podcast)
1034 return
1036 if widget.get_name() == 'treeAvailable':
1037 play_callback = lambda: self.playback_episode( current_channel, current_podcast)
1038 download_callback = lambda: self.download_podcast_by_url( url, want_message_dialog, None)
1039 gpe = gPodderEpisode( episode = current_podcast, channel = current_channel, download_callback = download_callback, play_callback = play_callback, center_on_widget = self.treeAvailable)
1040 return
1042 if not os.path.exists( filename) and not services.download_status_manager.is_download_in_progress( current_podcast.url):
1043 download.DownloadThread( current_channel, current_podcast, self.notification).start()
1044 else:
1045 if want_message_dialog and os.path.exists( filename) and not current_podcast.file_type() == 'torrent':
1046 title = _('Episode already downloaded')
1047 message = _('You have already downloaded this episode. Click on the episode to play it.')
1048 self.show_message( message, title)
1049 elif want_message_dialog and not current_podcast.file_type() == 'torrent':
1050 title = _('Download in progress')
1051 message = _('You are currently downloading this episode. Please check the download status tab to check when the download is finished.')
1052 self.show_message( message, title)
1054 if os.path.exists( filename):
1055 log( 'Episode has already been downloaded.')
1056 current_channel.addDownloadedItem( current_podcast)
1057 self.updateComboBox()
1059 def on_gPodder_delete_event(self, widget, *args):
1060 """Called when the GUI wants to close the window
1061 Displays a confirmation dialog (and closes/hides gPodder)
1064 downloading = services.download_status_manager.has_items()
1066 # Only iconify if we are using the window's "X" button,
1067 # but not when we are using "Quit" in the menu or toolbar
1068 if not gl.config.on_quit_ask and gl.config.on_quit_systray and self.tray_icon and widget.name not in ('toolQuit', 'itemClose'):
1069 self.iconify_main_window()
1070 elif gl.config.on_quit_ask or downloading:
1071 if gpodder.interface == gpodder.MAEMO:
1072 result = self.show_confirmation(_('Do you really want to quit gPodder now?'))
1073 if result:
1074 self.close_gpodder()
1075 else:
1076 return True
1077 dialog = gtk.MessageDialog(self.gPodder, gtk.DIALOG_MODAL, gtk.MESSAGE_QUESTION, gtk.BUTTONS_NONE)
1078 dialog.add_button(gtk.STOCK_CANCEL, gtk.RESPONSE_CANCEL)
1079 if self.tray_icon:
1080 dialog.add_button(_('Hide gPodder'), gtk.RESPONSE_YES)
1081 dialog.add_button(gtk.STOCK_QUIT, gtk.RESPONSE_CLOSE)
1083 title = _('Quit gPodder')
1084 if downloading:
1085 message = _('You are downloading episodes. If you close gPodder now, the downloads will be aborted.')
1086 elif self.tray_icon:
1087 message = _('If you hide gPodder, it will continue to run in the system tray notification area.')
1088 else:
1089 message = _('Do you really want to quit gPodder now?')
1091 dialog.set_title(title)
1092 dialog.set_markup('<span weight="bold" size="larger">%s</span>\n\n%s'%(title, message))
1093 if not downloading:
1094 cb_ask = gtk.CheckButton(_("Don't ask me again"))
1095 dialog.vbox.pack_start(cb_ask)
1096 cb_ask.show_all()
1098 result = dialog.run()
1099 dialog.destroy()
1101 if result == gtk.RESPONSE_CLOSE:
1102 if not downloading and cb_ask.get_active() == True:
1103 gl.config.on_quit_ask = False
1104 gl.config.on_quit_systray = False
1105 self.close_gpodder()
1106 elif result == gtk.RESPONSE_YES:
1107 if not downloading and cb_ask.get_active() == True:
1108 gl.config.on_quit_ask = False
1109 gl.config.on_quit_systray = True
1110 self.iconify_main_window()
1111 else:
1112 self.close_gpodder()
1114 return True
1116 def close_gpodder(self):
1117 """ clean everything and exit properly
1119 if self.channels:
1120 if not save_channels(self.channels):
1121 self.show_message(_('Please check your permissions and free disk space.'), _('Error saving channel list'))
1123 services.download_status_manager.cancel_all()
1125 self.gtk_main_quit()
1126 sys.exit( 0)
1128 def get_old_episodes(self):
1129 episodes = []
1130 for channel in self.channels:
1131 for episode in channel.get_all_episodes():
1132 if episode.is_downloaded() and episode.is_old() and not episode.is_locked() and episode.is_played():
1133 episodes.append(episode)
1134 return episodes
1136 def for_each_selected_episode_url( self, callback):
1137 ( model, paths ) = self.treeAvailable.get_selection().get_selected_rows()
1138 for path in paths:
1139 url = model.get_value( model.get_iter( path), 0)
1140 try:
1141 callback( url)
1142 except:
1143 log( 'Warning: Error in for_each_selected_episode_url for URL %s', url, sender = self)
1144 self.active_channel.update_model()
1145 self.updateComboBox()
1147 def delete_episode_list( self, episodes, confirm = True):
1148 if len(episodes) == 0:
1149 return
1151 if len(episodes) == 1:
1152 message = _('Do you really want to delete this episode?')
1153 else:
1154 message = _('Do you really want to delete %d episodes?') % len(episodes)
1156 if confirm and self.show_confirmation( message, _('Delete episodes')) == False:
1157 return
1159 for episode in episodes:
1160 log('Deleting episode: %s', episode.title, sender = self)
1161 episode.delete_from_disk()
1163 self.download_status_updated()
1165 def on_itemRemoveOldEpisodes_activate( self, widget):
1166 columns = (
1167 ('title', _('Episode')),
1168 ('channel_prop', _('Channel')),
1169 ('filesize_prop', _('Size')),
1170 ('pubdate_prop', _('Released')),
1171 ('played_prop', _('Status')),
1172 ('age_prop', _('Downloaded')),
1175 selection_buttons = {
1176 _('Select played'): lambda episode: episode.is_played(),
1177 _('Select older than %d days') % gl.config.episode_old_age: lambda episode: episode.is_old(),
1180 instructions = _('Select the episodes you want to delete from your hard disk.')
1182 episodes = []
1183 selected = []
1184 for channel in self.channels:
1185 for episode in channel:
1186 if episode.is_downloaded() and not episode.is_locked():
1187 episodes.append( episode)
1188 selected.append( episode.is_played())
1190 gPodderEpisodeSelector( title = _('Remove old episodes'), instructions = instructions, \
1191 episodes = episodes, selected = selected, columns = columns, \
1192 stock_ok_button = gtk.STOCK_DELETE, callback = self.delete_episode_list, \
1193 selection_buttons = selection_buttons)
1195 def on_item_toggle_downloaded_activate( self, widget, toggle = True, new_value = False):
1196 if toggle:
1197 callback = lambda url: gl.history_mark_downloaded(url, not gl.history_is_downloaded(url))
1198 else:
1199 callback = lambda url: gl.history_mark_downloaded(url, new_value)
1201 self.for_each_selected_episode_url( callback)
1203 def on_item_toggle_played_activate( self, widget, toggle = True, new_value = False):
1204 if toggle:
1205 callback = lambda url: gl.history_mark_played(url, not gl.history_is_played(url))
1206 else:
1207 callback = lambda url: gl.history_mark_played(url, new_value)
1209 self.for_each_selected_episode_url( callback)
1211 def on_item_toggle_lock_activate(self, widget, toggle=True, new_value=False):
1212 if toggle:
1213 callback = lambda url: gl.history_mark_locked(url, not gl.history_is_locked(url))
1214 else:
1215 callback = lambda url: gl.history_mark_locked(url, new_value)
1217 self.for_each_selected_episode_url(callback)
1219 def on_itemUpdate_activate(self, widget, notify_no_new_episodes=False):
1220 if self.channels:
1221 self.update_feed_cache(notify_no_new_episodes=notify_no_new_episodes)
1222 else:
1223 title = _('No channels available')
1224 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.')
1225 self.show_message( message, title)
1227 def download_episode_list( self, episodes):
1228 for episode in episodes:
1229 log('Downloading episode: %s', episode.title, sender = self)
1230 filename = episode.local_filename()
1231 if not os.path.exists( filename) and not services.download_status_manager.is_download_in_progress( episode.url):
1232 download.DownloadThread( episode.channel, episode, self.notification).start()
1234 def new_episodes_show(self, episodes):
1235 columns = (
1236 ('title', _('Episode')),
1237 ('channel_prop', _('Channel')),
1238 ('filesize_prop', _('Size')),
1239 ('pubdate_prop', _('Released')),
1242 if len(episodes) > 0:
1243 instructions = _('Select the episodes you want to download now.')
1245 gPodderEpisodeSelector(title=_('New episodes available'), instructions=instructions, \
1246 episodes=episodes, columns=columns, selected_default=True, \
1247 callback=self.download_episode_list)
1248 else:
1249 title = _('No new episodes')
1250 message = _('No new episodes to download.\nPlease check for new episodes later.')
1251 self.show_message(message, title)
1253 def on_itemDownloadAllNew_activate(self, widget, *args):
1254 episodes = []
1255 for channel in self.channels:
1256 for episode in channel.get_new_episodes():
1257 episodes.append(episode)
1258 self.new_episodes_show(episodes)
1260 def on_sync_to_ipod_activate(self, widget, episodes=None):
1261 Thread(target=self.sync_to_ipod_thread, args=(widget, episodes)).start()
1263 def sync_to_ipod_thread(self, widget, episodes=None):
1264 device = sync.open_device()
1266 if device is None:
1267 title = _('No device configured')
1268 message = _('To use the synchronization feature, please configure your device in the preferences dialog first.')
1269 self.notification(message, title)
1270 return
1272 if not device.open():
1273 title = _('Cannot open device')
1274 message = _('There has been an error opening your device.')
1275 self.notification(message, title)
1276 return
1278 gPodderSync(device=device, gPodder=self)
1279 if self.tray_icon:
1280 self.tray_icon.set_synchronisation_device(device)
1282 if episodes is None:
1283 episodes_to_sync = []
1284 for channel in self.channels:
1285 if not channel.sync_to_devices:
1286 log('Skipping channel: %s', channel.title, sender=self)
1287 continue
1289 for episode in channel.get_all_episodes():
1290 if episode.is_downloaded():
1291 episodes_to_sync.append(episode)
1292 device.add_tracks(episodes_to_sync)
1293 else:
1294 device.add_tracks(episodes, force_played=True)
1296 if not device.close():
1297 title = _('Error closing device')
1298 message = _('There has been an error closing your device.')
1299 self.notification(message, title)
1300 return
1302 if self.tray_icon:
1303 self.tray_icon.release_synchronisation_device()
1305 # update model for played state updates after sync
1306 for channel in self.channels:
1307 util.idle_add(channel.update_model)
1308 util.idle_add(self.updateComboBox)
1310 def ipod_cleanup_callback(self, device, tracks):
1311 title = _('Delete podcasts from device?')
1312 message = _('Do you really want to completely remove the selected episodes?')
1313 if len(tracks) > 0 and self.show_confirmation(message, title):
1314 device.remove_tracks(tracks)
1316 if not device.close():
1317 title = _('Error closing device')
1318 message = _('There has been an error closing your device.')
1319 self.show_message(message, title)
1320 return
1322 def on_cleanup_ipod_activate(self, widget, *args):
1323 columns = (
1324 ('title', _('Episode')),
1325 ('filesize', _('Size')),
1326 ('modified', _('Copied')),
1327 ('playcount', _('Play count')),
1330 device = sync.open_device()
1332 if device is None:
1333 title = _('No device configured')
1334 message = _('To use the synchronization feature, please configure your device in the preferences dialog first.')
1335 self.show_message(message, title)
1336 return
1338 if not device.open():
1339 title = _('Cannot open device')
1340 message = _('There has been an error opening your device.')
1341 self.show_message(message, title)
1342 return
1344 gPodderSync(device=device, gPodder=self)
1346 tracks = device.get_all_tracks()
1347 if len(tracks) > 0:
1348 remove_tracks_callback = lambda tracks: self.ipod_cleanup_callback(device, tracks)
1349 title = _('Remove podcasts from device')
1350 instructions = _('Select the podcast episodes you want to remove from your device.')
1351 gPodderEpisodeSelector(title=title, instructions=instructions, episodes=tracks, columns=columns, \
1352 stock_ok_button=gtk.STOCK_DELETE, callback=remove_tracks_callback)
1353 else:
1354 title = _('No files on device')
1355 message = _('The devices contains no files to be removed.')
1356 self.show_message(message, title)
1358 def show_hide_tray_icon(self):
1359 if gl.config.display_tray_icon and have_trayicon and self.tray_icon is None:
1360 self.tray_icon = trayicon.GPodderStatusIcon(self, scalable_dir)
1361 elif not gl.config.display_tray_icon and self.tray_icon is not None:
1362 self.tray_icon.set_visible(False)
1363 del self.tray_icon
1364 self.tray_icon = None
1366 if gl.config.minimize_to_tray and self.tray_icon:
1367 self.tray_icon.set_visible(self.minimized)
1368 elif self.tray_icon:
1369 self.tray_icon.set_visible(True)
1371 def update_view_settings(self):
1372 if gl.config.show_toolbar:
1373 self.toolbar.show_all()
1374 else:
1375 self.toolbar.hide_all()
1377 if self.episode_description_shown != gl.config.episode_list_descriptions:
1378 for channel in self.channels:
1379 channel.force_update_tree_model()
1380 self.updateTreeView()
1381 self.episode_description_shown = gl.config.episode_list_descriptions
1383 def on_itemShowToolbar_activate(self, widget):
1384 gl.config.show_toolbar = self.itemShowToolbar.get_active()
1385 self.update_view_settings()
1387 def on_itemShowDescription_activate(self, widget):
1388 gl.config.episode_list_descriptions = self.itemShowDescription.get_active()
1389 self.update_view_settings()
1391 def update_item_device( self):
1392 if gl.config.device_type != 'none':
1393 self.itemDevice.show_all()
1394 ( label, image ) = self.itemDevice.get_children()
1395 label.set_text( gl.get_device_name())
1396 else:
1397 self.itemDevice.hide_all()
1399 def properties_closed( self):
1400 self.show_hide_tray_icon()
1401 self.update_item_device()
1402 self.updateComboBox()
1404 def on_itemPreferences_activate(self, widget, *args):
1405 gPodderProperties(callback_finished=self.properties_closed, user_apps_reader=self.user_apps_reader)
1407 def on_itemAddChannel_activate(self, widget, *args):
1408 if self.channelPaned.get_position() < 200:
1409 self.channelPaned.set_position( 200)
1410 self.entryAddChannel.set_text( _('Enter podcast URL'))
1411 self.entryAddChannel.grab_focus()
1413 def on_itemEditChannel_activate(self, widget, *args):
1414 if self.active_channel is None:
1415 title = _('No channel selected')
1416 message = _('Please select a channel in the channels list to edit.')
1417 self.show_message( message, title)
1418 return
1420 gPodderChannel(channel=self.active_channel, callback_closed=self.updateComboBox, callback_change_url=self.change_channel_url)
1422 def change_channel_url(self, old_url, new_url):
1423 channel = None
1424 try:
1425 channel = podcastChannel.get_by_url(url=new_url, force_update=True)
1426 except:
1427 channel = None
1429 if channel is None:
1430 self.show_message(_('The specified URL is invalid. The old URL has been used instead.'), _('Invalid URL'))
1431 return
1433 for channel in self.channels:
1434 if channel.url == old_url:
1435 log('=> change channel url from %s to %s', old_url, new_url)
1436 old_save_dir = channel.save_dir
1437 channel.url = new_url
1438 new_save_dir = channel.save_dir
1439 log('old save dir=%s', old_save_dir, sender=self)
1440 log('new save dir=%s', new_save_dir, sender=self)
1441 files = glob.glob(os.path.join(old_save_dir, '*'))
1442 log('moving %d files to %s', len(files), new_save_dir, sender=self)
1443 for file in files:
1444 log('moving %s', file, sender=self)
1445 shutil.move(file, new_save_dir)
1446 try:
1447 os.rmdir(old_save_dir)
1448 except:
1449 log('Warning: cannot delete %s', old_save_dir, sender=self)
1451 save_channels(self.channels)
1452 self.update_feed_cache(force_update=False)
1454 def on_itemRemoveChannel_activate(self, widget, *args):
1455 try:
1456 if gpodder.interface == gpodder.GUI:
1457 dialog = gtk.MessageDialog(self.gPodder, gtk.DIALOG_MODAL, gtk.MESSAGE_QUESTION, gtk.BUTTONS_NONE)
1458 dialog.add_button(gtk.STOCK_NO, gtk.RESPONSE_NO)
1459 dialog.add_button(gtk.STOCK_YES, gtk.RESPONSE_YES)
1461 title = _('Remove channel and episodes?')
1462 message = _('Do you really want to remove <b>%s</b> and all downloaded episodes?') % saxutils.escape(self.active_channel.title)
1464 dialog.set_title(title)
1465 dialog.set_markup('<span weight="bold" size="larger">%s</span>\n\n%s'%(title, message))
1467 cb_ask = gtk.CheckButton(_('Do not delete my downloaded episodes'))
1468 dialog.vbox.pack_start(cb_ask)
1469 cb_ask.show_all()
1470 affirmative = gtk.RESPONSE_YES
1471 elif gpodder.interface == gpodder.MAEMO:
1472 cb_ask = gtk.CheckButton('') # dummy check button
1473 dialog = hildon.Note('confirmation', (self.gPodder, _('Do you really want to remove this channel and all downloaded episodes?')))
1474 affirmative = gtk.RESPONSE_OK
1476 result = dialog.run()
1477 dialog.destroy()
1479 if result == affirmative:
1480 # delete downloaded episodes only if checkbox is unchecked
1481 if cb_ask.get_active() == False:
1482 self.active_channel.remove_downloaded()
1483 else:
1484 log('Not removing downloaded episodes', sender=self)
1486 # only delete partial files if we do not have any downloads in progress
1487 delete_partial = not services.download_status_manager.has_items()
1488 gl.clean_up_downloads(delete_partial)
1489 self.channels.remove(self.active_channel)
1490 save_channels(self.channels)
1491 if len(self.channels) > 0:
1492 self.treeChannels.get_selection().select_path((len(self.channels)-1,))
1493 self.active_channel = self.channels[len(self.channels)-1]
1494 self.update_feed_cache(force_update=False)
1495 except:
1496 log('There has been an error removing the channel.', traceback=True, sender=self)
1498 def on_itemExportChannels_activate(self, widget, *args):
1499 if not self.channels:
1500 title = _('Nothing to export')
1501 message = _('Your list of channel subscriptions is empty. Please subscribe to some podcasts first before trying to export your subscription list.')
1502 self.show_message( message, title)
1503 return
1505 dlg = gtk.FileChooserDialog( title=_("Export to OPML"), parent = None, action = gtk.FILE_CHOOSER_ACTION_SAVE)
1506 dlg.add_button( gtk.STOCK_CANCEL, gtk.RESPONSE_CANCEL)
1507 dlg.add_button( gtk.STOCK_SAVE, gtk.RESPONSE_OK)
1508 response = dlg.run()
1509 if response == gtk.RESPONSE_OK:
1510 filename = dlg.get_filename()
1511 exporter = opml.Exporter( filename)
1512 if not exporter.write( self.channels):
1513 self.show_message( _('Could not export OPML to file. Please check your permissions.'), _('OPML export failed'))
1515 dlg.destroy()
1517 def on_itemImportChannels_activate(self, widget, *args):
1518 gPodderOpmlLister().get_channels_from_url(gl.config.opml_url, lambda url: self.add_new_channel(url,False), lambda: self.on_itemDownloadAllNew_activate(self.gPodder))
1520 def on_btnTransfer_clicked(self, widget, *args):
1521 self.on_treeAvailable_row_activated( widget, args)
1523 def on_homepage_activate(self, widget, *args):
1524 util.open_website(app_website)
1526 def on_wishlist_activate(self, widget, *args):
1527 util.open_website('http://www.amazon.de/gp/registry/2PD2MYGHE6857')
1529 def on_wiki_activate(self, widget, *args):
1530 util.open_website('http://wiki.gpodder.org/')
1532 def on_bug_tracker_activate(self, widget, *args):
1533 util.open_website('http://bugs.gpodder.org/')
1535 def on_itemAbout_activate(self, widget, *args):
1536 dlg = gtk.AboutDialog()
1537 dlg.set_name(app_name.replace('p', 'P')) # gpodder->gPodder
1538 dlg.set_version( app_version)
1539 dlg.set_copyright( app_copyright)
1540 dlg.set_website( app_website)
1541 dlg.set_translator_credits( _('translator-credits'))
1542 dlg.connect( 'response', lambda dlg, response: dlg.destroy())
1544 if gpodder.interface == gpodder.GUI:
1545 # For the "GUI" version, we add some more
1546 # items to the about dialog (credits and logo)
1547 dlg.set_authors(app_authors)
1548 try:
1549 dlg.set_logo(gtk.gdk.pixbuf_new_from_file_at_size(scalable_dir, 200, 200))
1550 except:
1551 pass
1553 dlg.run()
1555 def on_wNotebook_switch_page(self, widget, *args):
1556 page_num = args[1]
1557 if gpodder.interface == gpodder.MAEMO:
1558 page = self.wNotebook.get_nth_page(page_num)
1559 self.set_title(self.wNotebook.get_tab_label(page).get_text())
1560 if page_num == 0:
1561 self.play_or_download()
1562 else:
1563 self.toolDownload.set_sensitive( False)
1564 self.toolPlay.set_sensitive( False)
1565 self.toolTransfer.set_sensitive( False)
1566 self.toolCancel.set_sensitive( services.download_status_manager.has_items())
1568 def on_treeChannels_row_activated(self, widget, *args):
1569 self.on_itemEditChannel_activate( self.treeChannels)
1571 def on_treeChannels_cursor_changed(self, widget, *args):
1572 ( model, iter ) = self.treeChannels.get_selection().get_selected()
1574 if model != None and iter != None:
1575 id = model.get_path( iter)[0]
1576 self.active_channel = self.channels[id]
1578 self.itemEditChannel.get_child().set_text( _('Edit "%s"') % ( self.active_channel.title,))
1579 self.itemRemoveChannel.get_child().set_text( _('Remove "%s"') % ( self.active_channel.title,))
1580 if gpodder.interface == gpodder.MAEMO:
1581 self.label2.set_text(self.active_channel.title)
1582 self.set_title(self.active_channel.title)
1583 self.itemEditChannel.show_all()
1584 self.itemRemoveChannel.show_all()
1585 else:
1586 self.active_channel = None
1587 self.itemEditChannel.hide_all()
1588 self.itemRemoveChannel.hide_all()
1590 self.updateTreeView()
1592 def on_entryAddChannel_changed(self, widget, *args):
1593 active = self.entryAddChannel.get_text() not in ('', _('Enter podcast URL'))
1594 self.btnAddChannel.set_sensitive( active)
1596 def on_btnAddChannel_clicked(self, widget, *args):
1597 url = self.entryAddChannel.get_text()
1598 self.entryAddChannel.set_text('')
1599 self.add_new_channel( url)
1601 def on_btnEditChannel_clicked(self, widget, *args):
1602 self.on_itemEditChannel_activate( widget, args)
1604 def on_treeAvailable_row_activated(self, widget, *args):
1605 try:
1606 selection = self.treeAvailable.get_selection()
1607 selection_tuple = selection.get_selected_rows()
1608 transfer_files = False
1609 episodes = []
1611 if selection.count_selected_rows() > 1:
1612 widget_to_send = None
1613 show_message_dialog = False
1614 else:
1615 widget_to_send = widget
1616 show_message_dialog = True
1618 if widget.get_name() == 'itemTransferSelected' or widget.get_name() == 'toolTransfer':
1619 transfer_files = True
1621 for apath in selection_tuple[1]:
1622 selection_iter = self.treeAvailable.get_model().get_iter( apath)
1623 url = self.treeAvailable.get_model().get_value( selection_iter, 0)
1625 if transfer_files:
1626 episodes.append( self.active_channel.find_episode( url))
1627 else:
1628 self.download_podcast_by_url( url, show_message_dialog, widget_to_send)
1630 if transfer_files and len(episodes):
1631 self.on_sync_to_ipod_activate(None, episodes)
1632 except:
1633 title = _('Nothing selected')
1634 message = _('Please select an episode that you want to download and then click on the download button to start downloading the selected episode.')
1635 self.show_message( message, title)
1637 def on_btnDownload_clicked(self, widget, *args):
1638 self.on_treeAvailable_row_activated( widget, args)
1640 def on_treeAvailable_button_release_event(self, widget, *args):
1641 self.play_or_download()
1643 def on_btnDownloadNewer_clicked(self, widget, *args):
1644 self.new_episodes_show(self.active_channel.get_new_episodes())
1646 def on_btnSelectAllAvailable_clicked(self, widget, *args):
1647 self.treeAvailable.get_selection().select_all()
1648 self.on_treeAvailable_row_activated( self.toolDownload, args)
1649 self.treeAvailable.get_selection().unselect_all()
1651 def auto_update_procedure(self, first_run=False):
1652 log('auto_update_procedure() got called', sender=self)
1653 if not first_run and gl.config.auto_update_feeds and self.minimized:
1654 self.update_feed_cache()
1656 next_update = 60*1000*gl.config.auto_update_frequency
1657 gobject.timeout_add(next_update, self.auto_update_procedure)
1659 def on_treeDownloads_row_activated(self, widget, *args):
1660 cancel_urls = []
1662 if self.wNotebook.get_current_page() > 0:
1663 # Use the download list treeview + model
1664 ( tree, column ) = ( self.treeDownloads, 3 )
1665 else:
1666 # Use the available podcasts treeview + model
1667 ( tree, column ) = ( self.treeAvailable, 0 )
1669 selection = tree.get_selection()
1670 (model, paths) = selection.get_selected_rows()
1671 for path in paths:
1672 url = model.get_value( model.get_iter( path), column)
1673 cancel_urls.append( url)
1675 if len( cancel_urls) == 0:
1676 log('Nothing selected.', sender = self)
1677 return
1679 if len( cancel_urls) == 1:
1680 title = _('Cancel download?')
1681 message = _("Cancelling this download will remove the partially downloaded file and stop the download.")
1682 else:
1683 title = _('Cancel downloads?')
1684 message = _("Cancelling the download will stop the %d selected downloads and remove partially downloaded files.") % selection.count_selected_rows()
1686 if self.show_confirmation( message, title):
1687 for url in cancel_urls:
1688 services.download_status_manager.cancel_by_url( url)
1690 def on_btnCancelDownloadStatus_clicked(self, widget, *args):
1691 self.on_treeDownloads_row_activated( widget, None)
1693 def on_btnCancelAll_clicked(self, widget, *args):
1694 self.treeDownloads.get_selection().select_all()
1695 self.on_treeDownloads_row_activated( self.toolCancel, None)
1696 self.treeDownloads.get_selection().unselect_all()
1698 def on_btnDownloadedExecute_clicked(self, widget, *args):
1699 self.on_treeAvailable_row_activated( widget, args)
1701 def on_btnDownloadedDelete_clicked(self, widget, *args):
1702 if self.active_channel is None:
1703 return
1705 channel_url = self.active_channel.url
1706 selection = self.treeAvailable.get_selection()
1707 ( model, paths ) = selection.get_selected_rows()
1709 if selection.count_selected_rows() == 0:
1710 log( 'Nothing selected - will not remove any downloaded episode.')
1711 return
1713 if selection.count_selected_rows() == 1:
1714 episode_title = saxutils.escape(model.get_value(model.get_iter(paths[0]), 1))
1716 locked = gl.history_is_locked(model.get_value(model.get_iter(paths[0]), 0))
1717 if locked:
1718 title = _('%s is locked') % episode_title
1719 message = _('You cannot delete this locked episode. You must unlock it before you can delete it.')
1720 self.notification(message, title)
1721 return
1723 title = _('Remove %s?') % episode_title
1724 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.")
1725 else:
1726 title = _('Remove %d episodes?') % selection.count_selected_rows()
1727 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.')
1729 locked_count = 0
1730 for path in paths:
1731 url = model.get_value(model.get_iter(path), 0)
1732 if gl.history_is_locked(url):
1733 locked_count += 1
1735 if selection.count_selected_rows() == locked_count:
1736 title = _('Episodes are locked')
1737 message = _('The selected episodes are locked. Please unlock the episodes that you want to delete before trying to delete them.')
1738 self.notification(message, title)
1739 return
1740 elif locked_count > 0:
1741 title = _('Remove %d out of %d episodes?') % (selection.count_selected_rows() - locked_count, selection.count_selected_rows())
1742 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.')
1744 # if user confirms deletion, let's remove some stuff ;)
1745 if self.show_confirmation( message, title):
1746 try:
1747 # iterate over the selection, see also on_treeDownloads_row_activated
1748 for path in paths:
1749 url = model.get_value( model.get_iter( path), 0)
1750 self.active_channel.delete_episode_by_url( url)
1751 gl.history_mark_downloaded(url)
1753 # now, clear local db cache so we can re-read it
1754 self.updateComboBox()
1755 except:
1756 log( 'Error while deleting (some) downloads.')
1758 # only delete partial files if we do not have any downloads in progress
1759 delete_partial = not services.download_status_manager.has_items()
1760 gl.clean_up_downloads(delete_partial)
1761 self.active_channel.force_update_tree_model()
1762 self.updateTreeView()
1764 def on_btnDeleteAll_clicked(self, widget, *args):
1765 self.treeAvailable.get_selection().select_all()
1766 self.on_btnDownloadedDelete_clicked( widget, args)
1767 self.treeAvailable.get_selection().unselect_all()
1769 def on_key_press(self, widget, event):
1770 # Currently, we only handle Maemo hardware keys here,
1771 # so if we are not a Maemo app, we don't do anything!
1772 if gpodder.interface != gpodder.MAEMO:
1773 return
1775 if event.keyval == gtk.keysyms.F6:
1776 if self.fullscreen:
1777 self.window.unfullscreen()
1778 else:
1779 self.window.fullscreen()
1780 if event.keyval == gtk.keysyms.Escape:
1781 new_visibility = not self.vboxChannelNavigator.get_property('visible')
1782 self.vboxChannelNavigator.set_property('visible', new_visibility)
1783 self.column_size.set_visible(not new_visibility)
1784 self.column_released.set_visible(not new_visibility)
1786 diff = 0
1787 if event.keyval == gtk.keysyms.F7: #plus
1788 diff = 1
1789 elif event.keyval == gtk.keysyms.F8: #minus
1790 diff = -1
1792 if diff != 0:
1793 selection = self.treeChannels.get_selection()
1794 (model, iter) = selection.get_selected()
1795 selection.select_path(((model.get_path(iter)[0]+diff)%len(model),))
1796 self.on_treeChannels_cursor_changed(self.treeChannels)
1798 def window_state_event(self, widget, event):
1799 if event.new_window_state & gtk.gdk.WINDOW_STATE_FULLSCREEN:
1800 self.fullscreen = True
1801 else:
1802 self.fullscreen = False
1804 old_minimized = self.minimized
1806 if event.new_window_state & gtk.gdk.WINDOW_STATE_ICONIFIED:
1807 self.minimized = True
1808 else:
1809 self.minimized = False
1811 if old_minimized != self.minimized and self.tray_icon:
1812 self.gPodder.set_skip_taskbar_hint(self.minimized)
1813 elif not self.tray_icon:
1814 self.gPodder.set_skip_taskbar_hint(False)
1816 if gl.config.minimize_to_tray and self.tray_icon:
1817 self.tray_icon.set_visible(self.minimized)
1819 def uniconify_main_window(self):
1820 if self.minimized:
1821 self.gPodder.present()
1823 def iconify_main_window(self):
1824 if not self.minimized:
1825 self.gPodder.iconify()
1827 class gPodderChannel(GladeWidget):
1828 def new(self):
1829 global WEB_BROWSER_ICON
1830 self.image3167.set_property('icon-name', WEB_BROWSER_ICON)
1831 self.gPodderChannel.set_title( self.channel.title)
1832 self.entryTitle.set_text( self.channel.title)
1833 self.entryURL.set_text( self.channel.url)
1835 self.LabelDownloadTo.set_text( self.channel.save_dir)
1836 self.LabelWebsite.set_text( self.channel.link)
1838 self.channel.load_settings()
1839 self.cbNoSync.set_active( not self.channel.sync_to_devices)
1840 self.musicPlaylist.set_text(self.channel.device_playlist_name)
1841 if self.channel.username:
1842 self.FeedUsername.set_text( self.channel.username)
1843 if self.channel.password:
1844 self.FeedPassword.set_text( self.channel.password)
1846 self.on_btnClearCover_clicked( self.btnClearCover, delete_file = False)
1847 self.on_btnDownloadCover_clicked( self.btnDownloadCover, url = False)
1849 # Hide the website button if we don't have a valid URL
1850 if not self.channel.link:
1851 self.btn_website.hide_all()
1853 b = gtk.TextBuffer()
1854 b.set_text( self.channel.description)
1855 self.channel_description.set_buffer( b)
1857 #Add Drag and Drop Support
1858 flags = gtk.DEST_DEFAULT_ALL
1859 targets = [ ('text/uri-list', 0, 2), ('text/plain', 0, 4) ]
1860 actions = gtk.gdk.ACTION_DEFAULT | gtk.gdk.ACTION_COPY
1861 self.vboxCoverEditor.drag_dest_set( flags, targets, actions)
1862 self.vboxCoverEditor.connect( 'drag_data_received', self.drag_data_received)
1864 def on_btn_website_clicked(self, widget):
1865 util.open_website(self.channel.link)
1867 def on_btnClearCover_clicked( self, widget, delete_file = True):
1868 self.imgCover.clear()
1869 if delete_file:
1870 util.delete_file( self.channel.cover_file)
1871 self.btnClearCover.set_sensitive( os.path.exists( self.channel.cover_file))
1872 self.btnDownloadCover.set_sensitive( not os.path.exists( self.channel.cover_file) and bool(self.channel.image))
1873 self.labelCoverStatus.set_text( _('You can drag a cover file here.'))
1874 self.labelCoverStatus.show()
1876 def on_btnDownloadCover_clicked( self, widget, url = None):
1877 if url is None:
1878 url = self.channel.image
1880 if url != False:
1881 self.btnDownloadCover.set_sensitive( False)
1883 self.labelCoverStatus.show()
1884 gl.get_image_from_url(url, self.imgCover.set_from_pixbuf, self.labelCoverStatus.set_text, self.cover_download_finished, self.channel.cover_file)
1886 def cover_download_finished( self):
1887 self.labelCoverStatus.hide()
1888 self.btnClearCover.set_sensitive( os.path.exists( self.channel.cover_file))
1889 self.btnDownloadCover.set_sensitive( not os.path.exists( self.channel.cover_file) and bool(self.channel.image))
1891 def drag_data_received( self, widget, content, x, y, sel, ttype, time):
1892 files = sel.data.strip().split('\n')
1893 if len(files) != 1:
1894 self.show_message( _('You can only drop a single image or URL here.'), _('Drag and drop'))
1895 return
1897 file = files[0]
1899 if file.startswith( 'file://') or file.startswith( 'http://'):
1900 self.on_btnClearCover_clicked( self.btnClearCover)
1901 if file.startswith( 'file://'):
1902 filename = file[len('file://'):]
1903 shutil.copyfile( filename, self.channel.cover_file)
1904 self.on_btnDownloadCover_clicked( self.btnDownloadCover, url = file)
1905 return
1907 self.show_message( _('You can only drop local files and http:// URLs here.'), _('Drag and drop'))
1909 def on_gPodderChannel_destroy(self, widget, *args):
1910 self.callback_closed()
1912 def on_btnOK_clicked(self, widget, *args):
1913 entered_url = self.entryURL.get_text()
1914 channel_url = self.channel.url
1916 if entered_url != channel_url:
1917 if self.show_confirmation(_('Do you really want to move this channel to <b>%s</b>?') % (saxutils.escape(entered_url),), _('Really change URL?')):
1918 if hasattr(self, 'callback_change_url'):
1919 self.gPodderChannel.hide_all()
1920 self.callback_change_url(channel_url, entered_url)
1922 self.channel.sync_to_devices = not self.cbNoSync.get_active()
1923 self.channel.device_playlist_name = self.musicPlaylist.get_text()
1924 self.channel.set_custom_title( self.entryTitle.get_text())
1925 self.channel.username = self.FeedUsername.get_text().strip()
1926 self.channel.password = self.FeedPassword.get_text()
1927 self.channel.save_settings()
1929 self.gPodderChannel.destroy()
1932 class gPodderProperties(GladeWidget):
1933 def new(self):
1934 if not hasattr( self, 'callback_finished'):
1935 self.callback_finished = None
1937 if gpodder.interface == gpodder.MAEMO:
1938 self.table13.hide_all() # bluetooth
1939 self.table5.hide_all() # player
1940 self.table6.hide_all() # bittorrent
1941 # start from web importer
1942 self.hseparator3.hide_all()
1943 self.label87.hide_all()
1944 self.image2423.hide_all()
1945 self.opmlURL.hide_all()
1946 # end from web importer
1947 self.gPodderProperties.fullscreen()
1949 gl.config.connect_gtk_editable( 'http_proxy', self.httpProxy)
1950 gl.config.connect_gtk_editable( 'ftp_proxy', self.ftpProxy)
1951 gl.config.connect_gtk_editable( 'player', self.openApp)
1952 gl.config.connect_gtk_editable('videoplayer', self.openVideoApp)
1953 gl.config.connect_gtk_editable( 'opml_url', self.opmlURL)
1954 gl.config.connect_gtk_editable( 'custom_sync_name', self.entryCustomSyncName)
1955 gl.config.connect_gtk_togglebutton( 'custom_sync_name_enabled', self.cbCustomSyncName)
1956 gl.config.connect_gtk_togglebutton( 'auto_download_when_minimized', self.downloadnew)
1957 gl.config.connect_gtk_togglebutton( 'use_gnome_bittorrent', self.radio_gnome_bittorrent)
1958 gl.config.connect_gtk_togglebutton( 'update_on_startup', self.updateonstartup)
1959 gl.config.connect_gtk_togglebutton( 'only_sync_not_played', self.only_sync_not_played)
1960 gl.config.connect_gtk_togglebutton( 'fssync_channel_subfolders', self.cbChannelSubfolder)
1961 gl.config.connect_gtk_togglebutton( 'on_sync_mark_played', self.on_sync_mark_played)
1962 gl.config.connect_gtk_togglebutton( 'on_sync_delete', self.on_sync_delete)
1963 gl.config.connect_gtk_spinbutton( 'max_downloads', self.spinMaxDownloads)
1964 gl.config.connect_gtk_togglebutton( 'max_downloads_enabled', self.cbMaxDownloads)
1965 gl.config.connect_gtk_spinbutton( 'limit_rate_value', self.spinLimitDownloads)
1966 gl.config.connect_gtk_togglebutton( 'limit_rate', self.cbLimitDownloads)
1967 gl.config.connect_gtk_togglebutton( 'proxy_use_environment', self.cbEnvironmentVariables)
1968 gl.config.connect_gtk_filechooser( 'bittorrent_dir', self.chooserBitTorrentTo)
1969 gl.config.connect_gtk_spinbutton('episode_old_age', self.episode_old_age)
1970 gl.config.connect_gtk_togglebutton('auto_remove_old_episodes', self.auto_remove_old_episodes)
1971 gl.config.connect_gtk_togglebutton('auto_update_feeds', self.auto_update_feeds)
1972 gl.config.connect_gtk_spinbutton('auto_update_frequency', self.auto_update_frequency)
1973 gl.config.connect_gtk_togglebutton('display_tray_icon', self.display_tray_icon)
1974 gl.config.connect_gtk_togglebutton('minimize_to_tray', self.minimize_to_tray)
1975 gl.config.connect_gtk_togglebutton('enable_notifications', self.enable_notifications)
1976 gl.config.connect_gtk_togglebutton('start_iconified', self.start_iconified)
1977 gl.config.connect_gtk_togglebutton('on_quit_ask', self.on_quit_ask)
1978 gl.config.connect_gtk_togglebutton('bluetooth_enabled', self.bluetooth_enabled)
1979 gl.config.connect_gtk_togglebutton('bluetooth_ask_always', self.bluetooth_ask_always)
1980 gl.config.connect_gtk_togglebutton('bluetooth_ask_never', self.bluetooth_ask_never)
1981 gl.config.connect_gtk_togglebutton('bluetooth_use_converter', self.bluetooth_use_converter)
1982 gl.config.connect_gtk_filechooser( 'bluetooth_converter', self.bluetooth_converter, is_for_files=True)
1983 gl.config.connect_gtk_togglebutton('ipod_write_gtkpod_extended', self.ipod_write_gtkpod_extended)
1985 self.enable_notifications.set_sensitive(self.display_tray_icon.get_active())
1986 self.minimize_to_tray.set_sensitive(self.display_tray_icon.get_active())
1988 self.entryCustomSyncName.set_sensitive( self.cbCustomSyncName.get_active())
1989 self.radio_copy_torrents.set_active( not self.radio_gnome_bittorrent.get_active())
1991 self.iPodMountpoint.set_label( gl.config.ipod_mount)
1992 self.filesystemMountpoint.set_label( gl.config.mp3_player_folder)
1993 self.bluetooth_device_name.set_markup('<b>%s</b>'%gl.config.bluetooth_device_name)
1994 self.chooserDownloadTo.set_current_folder(gl.downloaddir)
1996 if tagging_supported():
1997 gl.config.connect_gtk_togglebutton( 'update_tags', self.updatetags)
1998 else:
1999 self.updatetags.set_sensitive( False)
2000 new_label = '%s (%s)' % ( self.updatetags.get_label(), _('needs python-eyed3') )
2001 self.updatetags.set_label( new_label)
2003 # device type
2004 self.comboboxDeviceType.set_active( 0)
2005 if gl.config.device_type == 'ipod':
2006 self.comboboxDeviceType.set_active( 1)
2007 elif gl.config.device_type == 'filesystem':
2008 self.comboboxDeviceType.set_active( 2)
2010 # setup cell renderers
2011 cellrenderer = gtk.CellRendererPixbuf()
2012 self.comboAudioPlayerApp.pack_start(cellrenderer, False)
2013 self.comboAudioPlayerApp.add_attribute(cellrenderer, 'pixbuf', 2)
2014 cellrenderer = gtk.CellRendererText()
2015 self.comboAudioPlayerApp.pack_start(cellrenderer, True)
2016 self.comboAudioPlayerApp.add_attribute(cellrenderer, 'markup', 0)
2018 cellrenderer = gtk.CellRendererPixbuf()
2019 self.comboVideoPlayerApp.pack_start(cellrenderer, False)
2020 self.comboVideoPlayerApp.add_attribute(cellrenderer, 'pixbuf', 2)
2021 cellrenderer = gtk.CellRendererText()
2022 self.comboVideoPlayerApp.pack_start(cellrenderer, True)
2023 self.comboVideoPlayerApp.add_attribute(cellrenderer, 'markup', 0)
2025 if not hasattr(self, 'user_apps_reader'):
2026 self.user_apps_reader = UserAppsReader(['audio', 'video'])
2028 if gpodder.interface == gpodder.GUI:
2029 self.user_apps_reader.read()
2031 self.comboAudioPlayerApp.set_model(self.user_apps_reader.get_applications_as_model('audio'))
2032 index = self.find_active_audio_app()
2033 self.comboAudioPlayerApp.set_active(index)
2034 self.comboVideoPlayerApp.set_model(self.user_apps_reader.get_applications_as_model('video'))
2035 index = self.find_active_video_app()
2036 self.comboVideoPlayerApp.set_active(index)
2038 self.ipodIcon.set_from_icon_name( 'gnome-dev-ipod', gtk.ICON_SIZE_BUTTON)
2040 def update_mountpoint( self, ipod):
2041 if ipod is None or ipod.mount_point is None:
2042 self.iPodMountpoint.set_label( '')
2043 else:
2044 self.iPodMountpoint.set_label( ipod.mount_point)
2046 def on_bluetooth_select_device_clicked(self, widget):
2047 # Stupid GTK doesn't provide us with a method to directly
2048 # edit the text of a gtk.Button without "destroying" the
2049 # image on it, so we dig into the button's widget tree and
2050 # get the gtk.Image and gtk.Label and edit the label directly.
2051 alignment = self.bluetooth_select_device.get_child()
2052 hbox = alignment.get_child()
2053 (image, label) = hbox.get_children()
2055 old_text = label.get_text()
2056 label.set_text(_('Searching...'))
2057 self.bluetooth_select_device.set_sensitive(False)
2058 while gtk.events_pending():
2059 gtk.main_iteration(False)
2061 # FIXME: Make bluetooth device discovery threaded, so
2062 # the GUI doesn't freeze while we are searching for devices
2063 found = False
2064 for name, address in util.discover_bluetooth_devices():
2065 if self.show_confirmation('Use this device as your bluetooth device?', name):
2066 gl.config.bluetooth_device_name = name
2067 gl.config.bluetooth_device_address = address
2068 self.bluetooth_device_name.set_markup('<b>%s</b>'%gl.config.bluetooth_device_name)
2069 found = True
2070 break
2071 if not found:
2072 self.show_message('No more devices found', 'Scan finished')
2073 self.bluetooth_select_device.set_sensitive(True)
2074 label.set_text(old_text)
2076 def find_active_audio_app(self):
2077 model = self.comboAudioPlayerApp.get_model()
2078 iter = model.get_iter_first()
2079 index = 0
2080 while iter is not None:
2081 command = model.get_value(iter, 1)
2082 if command == self.openApp.get_text():
2083 return index
2084 iter = model.iter_next(iter)
2085 index += 1
2086 # return last item = custom command
2087 return index-1
2089 def find_active_video_app( self):
2090 model = self.comboVideoPlayerApp.get_model()
2091 iter = model.get_iter_first()
2092 index = 0
2093 while iter is not None:
2094 command = model.get_value(iter, 1)
2095 if command == self.openVideoApp.get_text():
2096 return index
2097 iter = model.iter_next(iter)
2098 index += 1
2099 # return last item = custom command
2100 return index-1
2102 def set_download_dir( self, new_download_dir, event = None):
2103 gl.downloaddir = self.chooserDownloadTo.get_filename()
2104 if gl.downloaddir != self.chooserDownloadTo.get_filename():
2105 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'))
2107 if event:
2108 event.set()
2110 def on_auto_update_feeds_toggled( self, widget, *args):
2111 self.auto_update_frequency.set_sensitive(widget.get_active())
2113 def on_display_tray_icon_toggled( self, widget, *args):
2114 self.enable_notifications.set_sensitive(widget.get_active())
2115 self.minimize_to_tray.set_sensitive(widget.get_active())
2117 def on_cbCustomSyncName_toggled( self, widget, *args):
2118 self.entryCustomSyncName.set_sensitive( widget.get_active())
2120 def on_btnCustomSyncNameHelp_clicked( self, widget):
2121 examples = [
2122 '<i>{episode.title}</i> -&gt; <b>Interview with RMS</b>',
2123 '<i>{episode.basename}</i> -&gt; <b>70908-interview-rms</b>',
2124 '<i>{episode.published}</i> -&gt; <b>20070908</b>'
2127 info = [
2128 _('You can specify a custom format string for the file names on your MP3 player here.'),
2129 _('The format string will be used to generate a file name on your device. The file extension (e.g. ".mp3") will be added automatically.'),
2130 '\n'.join( [ ' %s' % s for s in examples ])
2133 self.show_message( '\n\n'.join( info), _('Custom format strings'))
2135 def on_gPodderProperties_destroy(self, widget, *args):
2136 self.on_btnOK_clicked( widget, *args)
2138 def on_btnConfigEditor_clicked(self, widget, *args):
2139 self.on_btnOK_clicked(widget, *args)
2140 gPodderConfigEditor()
2142 def on_comboAudioPlayerApp_changed(self, widget, *args):
2143 # find out which one
2144 iter = self.comboAudioPlayerApp.get_active_iter()
2145 model = self.comboAudioPlayerApp.get_model()
2146 command = model.get_value( iter, 1)
2147 if command == '':
2148 self.openApp.set_sensitive( True)
2149 self.openApp.show()
2150 self.labelCustomCommand.show()
2151 else:
2152 self.openApp.set_text( command)
2153 self.openApp.set_sensitive( False)
2154 self.openApp.hide()
2155 self.labelCustomCommand.hide()
2157 def on_comboVideoPlayerApp_changed(self, widget, *args):
2158 # find out which one
2159 iter = self.comboVideoPlayerApp.get_active_iter()
2160 model = self.comboVideoPlayerApp.get_model()
2161 command = model.get_value(iter, 1)
2162 if command == '':
2163 self.openVideoApp.set_sensitive(True)
2164 self.openVideoApp.show()
2165 self.labelCustomCommand.show()
2166 else:
2167 self.openVideoApp.set_text(command)
2168 self.openVideoApp.set_sensitive(False)
2169 self.openVideoApp.hide()
2170 self.labelCustomCommand.hide()
2172 def on_cbMaxDownloads_toggled(self, widget, *args):
2173 self.spinMaxDownloads.set_sensitive( self.cbMaxDownloads.get_active())
2175 def on_cbLimitDownloads_toggled(self, widget, *args):
2176 self.spinLimitDownloads.set_sensitive( self.cbLimitDownloads.get_active())
2178 def on_cbEnvironmentVariables_toggled(self, widget, *args):
2179 sens = not self.cbEnvironmentVariables.get_active()
2180 self.httpProxy.set_sensitive( sens)
2181 self.ftpProxy.set_sensitive( sens)
2183 def on_comboboxDeviceType_changed(self, widget, *args):
2184 active_item = self.comboboxDeviceType.get_active()
2186 # None
2187 sync_widgets = ( self.only_sync_not_played, self.labelSyncOptions,
2188 self.imageSyncOptions, self. separatorSyncOptions,
2189 self.on_sync_mark_played, self.on_sync_delete,
2190 self.on_sync_leave, self.label_after_sync, )
2191 for widget in sync_widgets:
2192 if active_item == 0:
2193 widget.hide_all()
2194 else:
2195 widget.show_all()
2197 # iPod
2198 ipod_widgets = (self.ipodLabel, self.btn_iPodMountpoint,
2199 self.ipod_write_gtkpod_extended)
2200 for widget in ipod_widgets:
2201 if active_item == 1:
2202 widget.show_all()
2203 else:
2204 widget.hide_all()
2206 # filesystem-based MP3 player
2207 fs_widgets = ( self.filesystemLabel, self.btn_filesystemMountpoint,
2208 self.cbChannelSubfolder, self.cbCustomSyncName,
2209 self.entryCustomSyncName, self.btnCustomSyncNameHelp )
2210 for widget in fs_widgets:
2211 if active_item == 2:
2212 widget.show_all()
2213 else:
2214 widget.hide_all()
2216 def on_btn_iPodMountpoint_clicked(self, widget, *args):
2217 fs = gtk.FileChooserDialog( title = _('Select iPod mountpoint'), action = gtk.FILE_CHOOSER_ACTION_SELECT_FOLDER)
2218 fs.add_button( gtk.STOCK_CANCEL, gtk.RESPONSE_CANCEL)
2219 fs.add_button( gtk.STOCK_OPEN, gtk.RESPONSE_OK)
2220 fs.set_current_folder(self.iPodMountpoint.get_label())
2221 if fs.run() == gtk.RESPONSE_OK:
2222 self.iPodMountpoint.set_label( fs.get_filename())
2223 fs.destroy()
2225 def on_btn_FilesystemMountpoint_clicked(self, widget, *args):
2226 fs = gtk.FileChooserDialog( title = _('Select folder for MP3 player'), action = gtk.FILE_CHOOSER_ACTION_SELECT_FOLDER)
2227 fs.add_button( gtk.STOCK_CANCEL, gtk.RESPONSE_CANCEL)
2228 fs.add_button( gtk.STOCK_OPEN, gtk.RESPONSE_OK)
2229 fs.set_current_folder(self.filesystemMountpoint.get_label())
2230 if fs.run() == gtk.RESPONSE_OK:
2231 self.filesystemMountpoint.set_label( fs.get_filename())
2232 fs.destroy()
2234 def on_btnOK_clicked(self, widget, *args):
2235 gl.config.ipod_mount = self.iPodMountpoint.get_label()
2236 gl.config.mp3_player_folder = self.filesystemMountpoint.get_label()
2238 if gl.downloaddir != self.chooserDownloadTo.get_filename():
2239 new_download_dir = self.chooserDownloadTo.get_filename()
2240 download_dir_size = util.calculate_size( gl.downloaddir)
2241 download_dir_size_string = gl.format_filesize( download_dir_size)
2242 event = Event()
2244 dlg = gtk.Dialog( _('Moving downloads folder'), self.gPodderProperties)
2245 dlg.vbox.set_spacing( 5)
2246 dlg.set_border_width( 5)
2248 label = gtk.Label()
2249 label.set_line_wrap( True)
2250 label.set_markup( _('Moving downloads from <b>%s</b> to <b>%s</b>...') % ( saxutils.escape( gl.downloaddir), saxutils.escape( new_download_dir), ))
2251 myprogressbar = gtk.ProgressBar()
2253 # put it all together
2254 dlg.vbox.pack_start( label)
2255 dlg.vbox.pack_end( myprogressbar)
2257 # switch windows
2258 dlg.show_all()
2259 self.gPodderProperties.hide_all()
2261 # hide action area and separator line
2262 dlg.action_area.hide()
2263 dlg.set_has_separator( False)
2265 args = ( new_download_dir, event, )
2267 thread = Thread( target = self.set_download_dir, args = args)
2268 thread.start()
2270 while not event.isSet():
2271 try:
2272 new_download_dir_size = util.calculate_size( new_download_dir)
2273 except:
2274 new_download_dir_size = 0
2275 if download_dir_size > 0:
2276 fract = (1.00*new_download_dir_size) / (1.00*download_dir_size)
2277 else:
2278 fract = 0.0
2279 if fract < 0.99:
2280 myprogressbar.set_text( _('%s of %s') % ( gl.format_filesize( new_download_dir_size), download_dir_size_string, ))
2281 else:
2282 myprogressbar.set_text( _('Finishing... please wait.'))
2283 myprogressbar.set_fraction(max(0.0,min(1.0,fract)))
2284 event.wait( 0.1)
2285 while gtk.events_pending():
2286 gtk.main_iteration( False)
2288 dlg.destroy()
2290 device_type = self.comboboxDeviceType.get_active()
2291 if device_type == 0:
2292 gl.config.device_type = 'none'
2293 elif device_type == 1:
2294 gl.config.device_type = 'ipod'
2295 elif device_type == 2:
2296 gl.config.device_type = 'filesystem'
2297 self.gPodderProperties.destroy()
2298 if self.callback_finished:
2299 self.callback_finished()
2302 class gPodderEpisode(GladeWidget):
2303 def new(self):
2304 global WEB_BROWSER_ICON
2305 self.image3166.set_property('icon-name', WEB_BROWSER_ICON)
2306 services.download_status_manager.register( 'list-changed', self.on_download_status_changed)
2307 services.download_status_manager.register( 'progress-detail', self.on_download_status_progress)
2309 self.episode_title.set_markup( '<span weight="bold" size="larger">%s</span>' % saxutils.escape( self.episode.title))
2311 if gpodder.interface == gpodder.MAEMO:
2312 # Hide the advanced prefs expander
2313 self.expander1.hide_all()
2315 b = gtk.TextBuffer()
2316 b.set_text( strip( self.episode.description))
2317 self.episode_description.set_buffer( b)
2319 self.gPodderEpisode.set_title( self.episode.title)
2320 self.LabelDownloadLink.set_text( self.episode.url)
2321 self.LabelWebsiteLink.set_text( self.episode.link)
2322 self.labelPubDate.set_text( self.episode.pubDate)
2324 # Hide the "Go to website" button if we don't have a valid URL
2325 if self.episode.link == self.episode.url or not self.episode.link:
2326 self.btn_website.hide_all()
2328 self.channel_title.set_markup( _('<i>from %s</i>') % saxutils.escape( self.channel.title))
2330 self.hide_show_widgets()
2331 services.download_status_manager.request_progress_detail( self.episode.url)
2333 def on_btnCancel_clicked( self, widget):
2334 services.download_status_manager.cancel_by_url( self.episode.url)
2336 def on_gPodderEpisode_destroy( self, widget):
2337 services.download_status_manager.unregister( 'list-changed', self.on_download_status_changed)
2338 services.download_status_manager.unregister( 'progress-detail', self.on_download_status_progress)
2340 def on_download_status_changed( self):
2341 self.hide_show_widgets()
2343 def on_btn_website_clicked(self, widget):
2344 util.open_website(self.episode.link)
2346 def on_download_status_progress( self, url, progress, speed):
2347 if url == self.episode.url:
2348 progress = float(min(100.0,max(0.0,progress)))
2349 self.progress_bar.set_fraction(progress/100.0)
2350 self.progress_bar.set_text( 'Downloading: %d%% (%s)' % ( progress, speed, ))
2352 def hide_show_widgets( self):
2353 is_downloading = services.download_status_manager.is_download_in_progress( self.episode.url)
2354 if is_downloading:
2355 self.progress_bar.show_all()
2356 self.btnCancel.show_all()
2357 self.btnPlay.hide_all()
2358 self.btnSaveFile.hide_all()
2359 self.btnDownload.hide_all()
2360 else:
2361 self.progress_bar.hide_all()
2362 self.btnCancel.hide_all()
2363 if os.path.exists( self.episode.local_filename()):
2364 self.btnPlay.show_all()
2365 self.btnSaveFile.show_all()
2366 self.btnDownload.hide_all()
2367 else:
2368 self.btnPlay.hide_all()
2369 self.btnSaveFile.hide_all()
2370 self.btnDownload.show_all()
2372 def on_btnCloseWindow_clicked(self, widget, *args):
2373 self.gPodderEpisode.destroy()
2375 def on_btnDownload_clicked(self, widget, *args):
2376 if self.download_callback:
2377 self.download_callback()
2379 def on_btnPlay_clicked(self, widget, *args):
2380 if self.play_callback:
2381 self.play_callback()
2383 self.gPodderEpisode.destroy()
2385 def on_btnSaveFile_clicked(self, widget, *args):
2386 self.show_copy_dialog( src_filename = self.episode.local_filename(), dst_filename = self.episode.sync_filename())
2389 class gPodderSync(GladeWidget):
2390 def new(self):
2391 util.idle_add(self.imageSync.set_from_icon_name, 'gnome-dev-ipod', gtk.ICON_SIZE_DIALOG)
2393 self.device.register('progress', self.on_progress)
2394 self.device.register('sub-progress', self.on_sub_progress)
2395 self.device.register('status', self.on_status)
2396 self.device.register('done', self.on_done)
2398 def on_progress(self, pos, max):
2399 util.idle_add(self.progressbar.set_fraction, float(pos)/float(max))
2400 util.idle_add(self.progressbar.set_text, _('%d of %d done') % (pos, max))
2402 def on_sub_progress(self, percentage):
2403 util.idle_add(self.progressbar.set_text, _('Processing (%d%%)') % (percentage))
2405 def on_status(self, status):
2406 util.idle_add(self.status_label.set_markup, '<i>%s</i>' % saxutils.escape(status))
2408 def on_done(self):
2409 util.idle_add(self.gPodderSync.destroy)
2410 if not self.gPodder.minimized:
2411 util.idle_add(self.notification, _('Your device has been updated by gPodder.'), _('Operation finished'))
2413 def on_gPodderSync_destroy(self, widget, *args):
2414 self.device.unregister('progress', self.on_progress)
2415 self.device.unregister('sub-progress', self.on_sub_progress)
2416 self.device.unregister('status', self.on_status)
2417 self.device.unregister('done', self.on_done)
2418 self.device.cancel()
2420 def on_cancel_button_clicked(self, widget, *args):
2421 self.device.cancel()
2424 class gPodderOpmlLister(GladeWidget):
2425 def new(self):
2426 # initiate channels list
2427 self.channels = []
2428 self.callback_for_channel = None
2429 self.callback_finished = None
2431 togglecell = gtk.CellRendererToggle()
2432 togglecell.set_property( 'activatable', True)
2433 togglecell.connect( 'toggled', self.callback_edited)
2434 togglecolumn = gtk.TreeViewColumn( '', togglecell, active=0)
2436 titlecell = gtk.CellRendererText()
2437 titlecolumn = gtk.TreeViewColumn( _('Channel'), titlecell, markup=1)
2439 for itemcolumn in ( togglecolumn, titlecolumn ):
2440 self.treeviewChannelChooser.append_column( itemcolumn)
2442 def callback_edited( self, cell, path):
2443 model = self.treeviewChannelChooser.get_model()
2445 url = model[path][2]
2447 model[path][0] = not model[path][0]
2448 if model[path][0]:
2449 self.channels.append( url)
2450 else:
2451 self.channels.remove( url)
2453 self.btnOK.set_sensitive( bool(len(self.channels)))
2455 def thread_finished(self, model):
2456 self.treeviewChannelChooser.set_model(model)
2457 self.labelStatus.set_label('')
2458 self.btnDownloadOpml.set_sensitive(True)
2459 self.entryURL.set_sensitive(True)
2460 self.treeviewChannelChooser.set_sensitive(True)
2461 self.channels = []
2463 def thread_func(self):
2464 url = self.entryURL.get_text()
2465 importer = opml.Importer(url)
2466 model = importer.get_model()
2467 if len(model) == 0:
2468 self.notification(_('The specified URL does not provide any valid OPML podcast items.'), _('No feeds found'))
2469 util.idle_add(self.thread_finished, model)
2471 def get_channels_from_url( self, url, callback_for_channel = None, callback_finished = None):
2472 if callback_for_channel:
2473 self.callback_for_channel = callback_for_channel
2474 if callback_finished:
2475 self.callback_finished = callback_finished
2476 self.labelStatus.set_label( _('Downloading, please wait...'))
2477 self.entryURL.set_text( url)
2478 self.btnDownloadOpml.set_sensitive( False)
2479 self.entryURL.set_sensitive( False)
2480 self.btnOK.set_sensitive( False)
2481 self.treeviewChannelChooser.set_sensitive( False)
2482 Thread( target = self.thread_func).start()
2484 def on_gPodderOpmlLister_destroy(self, widget, *args):
2485 pass
2487 def on_btnDownloadOpml_clicked(self, widget, *args):
2488 self.get_channels_from_url( self.entryURL.get_text())
2490 def on_btnOK_clicked(self, widget, *args):
2491 self.gPodderOpmlLister.destroy()
2493 # add channels that have been selected
2494 for url in self.channels:
2495 if self.callback_for_channel:
2496 self.callback_for_channel( url)
2498 if self.callback_finished:
2499 self.callback_finished()
2501 def on_btnCancel_clicked(self, widget, *args):
2502 self.gPodderOpmlLister.destroy()
2505 class gPodderEpisodeSelector( GladeWidget):
2506 """Episode selection dialog
2508 Optional keyword arguments that modify the behaviour of this dialog:
2510 - callback: Function that takes 1 parameter which is a list of
2511 the selected episodes (or empty list when none selected)
2512 - episodes: List of episodes that are presented for selection
2513 - selected: (optional) List of boolean variables that define the
2514 default checked state for the given episodes
2515 - selected_default: (optional) The default boolean value for the
2516 checked state if no other value is set
2517 (default is False)
2518 - columns: List of (name,caption) pairs for the columns, the name
2519 is the attribute name of the episode to be read from
2520 each episode object and the caption attribute is the
2521 text that appear as column caption
2522 (default is [('title','Episode'),])
2523 - title: (optional) The title of the window + heading
2524 - instructions: (optional) A one-line text describing what the
2525 user should select / what the selection is for
2526 - stock_ok_button: (optional) Will replace the "OK" button with
2527 another GTK+ stock item to be used for the
2528 affirmative button of the dialog (e.g. can
2529 be gtk.STOCK_DELETE when the episodes to be
2530 selected will be deleted after closing the
2531 dialog)
2532 - selection_buttons: (optional) A dictionary with labels as
2533 keys and callbacks as values; for each
2534 key a button will be generated, and when
2535 the button is clicked, the callback will
2536 be called for each episode and the return
2537 value of the callback (True or False) will
2538 be the new selected state of the episode
2539 - size_attribute: (optional) The name of an attribute of the
2540 supplied episode objects that can be used to
2541 calculate the size of an episode; set this to
2542 None if no total size calculation should be
2543 done (in cases where total size is useless)
2544 (default is 'length')
2547 COLUMN_TOGGLE = 0
2548 COLUMN_ADDITIONAL = 1
2550 def new( self):
2551 if not hasattr( self, 'callback'):
2552 self.callback = None
2554 if not hasattr( self, 'episodes'):
2555 self.episodes = []
2557 if not hasattr( self, 'size_attribute'):
2558 self.size_attribute = 'length'
2560 if not hasattr( self, 'selection_buttons'):
2561 self.selection_buttons = {}
2563 if not hasattr( self, 'selected_default'):
2564 self.selected_default = False
2566 if not hasattr( self, 'selected'):
2567 self.selected = [self.selected_default]*len(self.episodes)
2569 if len(self.selected) < len(self.episodes):
2570 self.selected += [self.selected_default]*(len(self.episodes)-len(self.selected))
2572 if not hasattr( self, 'columns'):
2573 self.columns = ( ('title', _('Episode')), )
2575 if hasattr( self, 'title'):
2576 self.gPodderEpisodeSelector.set_title( self.title)
2577 self.labelHeading.set_markup( '<b><big>%s</big></b>' % saxutils.escape( self.title))
2579 if hasattr( self, 'instructions'):
2580 self.labelInstructions.set_text( self.instructions)
2581 self.labelInstructions.show_all()
2583 if hasattr( self, 'stock_ok_button'):
2584 self.btnOK.set_label( self.stock_ok_button)
2585 self.btnOK.set_use_stock( True)
2587 toggle_cell = gtk.CellRendererToggle()
2588 toggle_cell.connect( 'toggled', self.toggle_cell_handler)
2590 self.treeviewEpisodes.append_column( gtk.TreeViewColumn( '', toggle_cell, active=self.COLUMN_TOGGLE))
2592 next_column = self.COLUMN_ADDITIONAL
2593 for name, caption in self.columns:
2594 renderer = gtk.CellRendererText()
2595 if next_column > self.COLUMN_ADDITIONAL:
2596 renderer.set_property( 'ellipsize', pango.ELLIPSIZE_END)
2597 column = gtk.TreeViewColumn( caption, renderer, text=next_column)
2598 column.set_resizable( True)
2599 column.set_expand( True)
2600 self.treeviewEpisodes.append_column( column)
2601 next_column += 1
2603 column_types = [ gobject.TYPE_BOOLEAN ] + [ gobject.TYPE_STRING ] * len(self.columns)
2604 self.model = gtk.ListStore( *column_types)
2606 for index, episode in enumerate( self.episodes):
2607 row = [ self.selected[index] ]
2608 for name, caption in self.columns:
2609 row.append( getattr( episode, name))
2610 self.model.append( row)
2612 for label in self.selection_buttons:
2613 button = gtk.Button( label)
2614 button.connect('clicked', self.custom_selection_button_clicked, label)
2615 self.hboxButtons.pack_start( button, expand = False)
2616 button.show_all()
2618 self.treeviewEpisodes.set_rules_hint( True)
2619 self.treeviewEpisodes.set_model( self.model)
2620 self.treeviewEpisodes.columns_autosize()
2621 self.calculate_total_size()
2623 def calculate_total_size( self):
2624 if self.size_attribute is not None:
2625 total_size = 0
2626 for index, row in enumerate( self.model):
2627 if self.model.get_value( row.iter, self.COLUMN_TOGGLE) == True:
2628 try:
2629 total_size += int(getattr( self.episodes[index], self.size_attribute))
2630 except:
2631 log( 'Cannot get size for %s', self.episodes[index].title, sender = self)
2633 if total_size > 0:
2634 self.labelTotalSize.set_text( _('Total size: %s') % gl.format_filesize( total_size))
2635 else:
2636 self.labelTotalSize.set_text( '')
2637 self.labelTotalSize.show_all()
2638 else:
2639 self.labelTotalSize.hide_all()
2641 def toggle_cell_handler( self, cell, path):
2642 model = self.treeviewEpisodes.get_model()
2643 model[path][self.COLUMN_TOGGLE] = not model[path][self.COLUMN_TOGGLE]
2645 if self.size_attribute is not None:
2646 self.calculate_total_size()
2648 def custom_selection_button_clicked(self, button, label):
2649 callback = self.selection_buttons[label]
2651 for index, row in enumerate( self.model):
2652 new_value = callback( self.episodes[index])
2653 self.model.set_value( row.iter, self.COLUMN_TOGGLE, new_value)
2655 self.calculate_total_size()
2657 def on_btnCheckAll_clicked( self, widget):
2658 for row in self.model:
2659 self.model.set_value( row.iter, self.COLUMN_TOGGLE, True)
2661 self.calculate_total_size()
2663 def on_btnCheckNone_clicked( self, widget):
2664 for row in self.model:
2665 self.model.set_value( row.iter, self.COLUMN_TOGGLE, False)
2667 self.calculate_total_size()
2669 def get_selected_episodes( self):
2670 selected_episodes = []
2672 for index, row in enumerate( self.model):
2673 if self.model.get_value( row.iter, self.COLUMN_TOGGLE) == True:
2674 selected_episodes.append( self.episodes[index])
2676 return selected_episodes
2678 def on_btnOK_clicked( self, widget):
2679 self.gPodderEpisodeSelector.destroy()
2680 if self.callback is not None:
2681 self.callback( self.get_selected_episodes())
2683 def on_btnCancel_clicked( self, widget):
2684 self.gPodderEpisodeSelector.destroy()
2685 if self.callback is not None:
2686 self.callback([])
2688 class gPodderConfigEditor(GladeWidget):
2689 def new(self):
2690 name_column = gtk.TreeViewColumn(_('Variable'))
2691 name_renderer = gtk.CellRendererText()
2692 name_column.pack_start(name_renderer)
2693 name_column.add_attribute(name_renderer, 'text', 0)
2694 name_column.add_attribute(name_renderer, 'weight', 5)
2695 self.configeditor.append_column(name_column)
2697 type_column = gtk.TreeViewColumn(_('Type'))
2698 type_renderer = gtk.CellRendererText()
2699 type_column.pack_start(type_renderer)
2700 type_column.add_attribute(type_renderer, 'text', 1)
2701 type_column.add_attribute(type_renderer, 'weight', 5)
2702 self.configeditor.append_column(type_column)
2704 value_column = gtk.TreeViewColumn(_('Value'))
2705 value_renderer = gtk.CellRendererText()
2706 value_column.pack_start(value_renderer)
2707 value_column.add_attribute(value_renderer, 'text', 2)
2708 value_column.add_attribute(value_renderer, 'editable', 4)
2709 value_column.add_attribute(value_renderer, 'weight', 5)
2710 value_renderer.connect('edited', self.value_edited)
2711 self.configeditor.append_column(value_column)
2713 self.model = gl.config.model()
2714 self.filter = self.model.filter_new()
2715 self.filter.set_visible_func(self.visible_func)
2717 self.configeditor.set_model(self.filter)
2718 self.configeditor.set_rules_hint(True)
2720 def visible_func(self, model, iter, user_data=None):
2721 text = self.entryFilter.get_text().lower()
2722 if text == '':
2723 return True
2724 else:
2725 # either the variable name or its value
2726 return (text in model.get_value(iter, 0).lower() or
2727 text in model.get_value(iter, 2).lower())
2729 def value_edited(self, renderer, path, new_text):
2730 model = self.configeditor.get_model()
2731 iter = model.get_iter(path)
2732 name = model.get_value(iter, 0)
2733 type_cute = model.get_value(iter, 1)
2735 if not gl.config.update_field(name, new_text):
2736 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))
2738 def on_entryFilter_changed(self, widget):
2739 self.filter.refilter()
2741 def on_btnShowAll_clicked(self, widget):
2742 self.entryFilter.set_text('')
2743 self.entryFilter.grab_focus()
2745 def on_configeditor_row_activated(self, treeview, path, view_column):
2746 model = treeview.get_model()
2747 it = model.get_iter(path)
2748 field_name = model.get_value(it, 0)
2749 field_type = model.get_value(it, 3)
2751 # Flip the boolean config flag
2752 if field_type == bool:
2753 gl.config.toggle_flag(field_name)
2755 def on_btnClose_clicked(self, widget):
2756 self.gPodderConfigEditor.destroy()
2759 def main():
2760 gobject.threads_init()
2761 gtk.window_set_default_icon_name( 'gpodder')
2763 gPodder().run()