Allow finger-friendly main menu items in Maemo
[gpodder.git] / src / gpodder / gui.py
blob7347f66066599bd781e5a9ff30b4133318e08817
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
30 import urllib
31 import urllib2
32 import datetime
34 from xml.sax import saxutils
36 from threading import Event
37 from threading import Thread
38 from threading import Semaphore
39 from string import strip
41 import gpodder
42 from gpodder import util
43 from gpodder import opml
44 from gpodder import services
45 from gpodder import sync
46 from gpodder import download
47 from gpodder import SimpleGladeApp
48 from gpodder import my
49 from gpodder.liblogger import log
50 from gpodder.dbsqlite import db
51 from gpodder import resolver
53 try:
54 from gpodder import trayicon
55 have_trayicon = True
56 except Exception, exc:
57 log('Warning: Could not import gpodder.trayicon.', traceback=True)
58 log('Warning: This probably means your PyGTK installation is too old!')
59 have_trayicon = False
61 from libpodcasts import podcastChannel
62 from libpodcasts import LocalDBReader
63 from libpodcasts import podcastItem
64 from libpodcasts import channels_to_model
65 from libpodcasts import update_channel_model_by_iter
66 from libpodcasts import load_channels
67 from libpodcasts import update_channels
68 from libpodcasts import save_channels
69 from libpodcasts import can_restore_from_opml
70 from libpodcasts import HTTPAuthError
72 from gpodder.libgpodder import gl
74 from libplayers import UserAppsReader
76 from libtagupdate import tagging_supported
78 if gpodder.interface == gpodder.GUI:
79 WEB_BROWSER_ICON = 'web-browser'
80 elif gpodder.interface == gpodder.MAEMO:
81 import hildon
82 WEB_BROWSER_ICON = 'qgn_toolb_browser_web'
84 app_name = "gpodder"
85 app_version = "unknown" # will be set in main() call
86 app_authors = [
87 _('Current maintainer:'), 'Thomas Perl <thpinfo.com>',
88 '',
89 _('Patches, bug reports and donations by:'), 'Adrien Beaucreux',
90 'Alain Tauch', 'Alistair Sutton', 'Anders Kvist', 'Andrei Dolganov', 'Andrew Bennett', 'Andy Busch',
91 'Antonio Roversi', 'Aravind Seshadri', 'Atte André Jensen', 'audioworld',
92 'Bastian Staeck', 'Bernd Schlapsi', 'Bill Barnard', 'Bill Peters', 'Bjørn Rasmussen', 'Camille Moncelier',
93 'Carlos Moffat', 'Chris Arnold', 'Clark Burbidge', 'Cory Albrecht', 'daggpod', 'Daniel Ramos',
94 'David Spreen', 'Doug Hellmann', 'Edouard Pellerin', 'FFranci72', 'Florian Richter', 'Frank Harper',
95 'Franz Seidl', 'FriedBunny', 'Gerrit Sangel', 'Gilles Lehoux', 'Götz Waschk',
96 'Haim Roitgrund', 'Heinz Erhard', 'Hex', 'Holger Bauer', 'Holger Leskien', 'Jens Thiele',
97 'Jérôme Chabod', 'Jerry Moss',
98 'Jessica Henline', 'João Trindade', 'Joel Calado', 'John Ferguson',
99 'José Luis Fustel', 'Joseph Bleau', 'Julio Acuña', 'Junio C Hamano',
100 'Jürgen Schinker', 'Justin Forest',
101 'Konstantin Ryabitsev', 'Leonid Ponomarev', 'Marcos Hernández', 'Mark Alford', 'Markus Golser', 'Mehmet Nur Olcay', 'Michael Salim',
102 'Mika Leppinen', 'Mike Coulson', 'Mikolaj Laczynski', 'Mykola Nikishov', 'narf',
103 'Nick L.', 'Nicolas Quienot', 'Ondrej Vesely',
104 'Ortwin Forster', 'Paul Elliot', 'Paul Rudkin',
105 'Pavel Mlčoch', 'Peter Hoffmann', 'PhilF', 'Philippe Gouaillier', 'Pieter de Decker',
106 'Preben Randhol', 'Rafael Proença', 'red26wings', 'Richard Voigt',
107 'Robert Young', 'Roel Groeneveld',
108 'Scott Wegner', 'Sebastian Krause', 'Seth Remington', 'Shane Donohoe', 'Silvio Sisto', 'SPGoetze',
109 'Stefan Lohmaier', 'Stephan Buys', 'Steve McCarthy', 'Stylianos Papanastasiou', 'Teo Ramirez',
110 'Thomas Matthijs', 'Thomas Mills Hinkle', 'Thomas Nilsson',
111 'Tim Michelsen', 'Tim Preetz', 'Todd Zullinger', 'Tomas Matheson', 'Ville-Pekka Vainio', 'Vitaliy Bondar', 'VladDrac',
112 'Vladimir Zemlyakov', 'Wilfred van Rooijen',
114 'List may be incomplete - please contact me.'
116 app_copyright = '© 2005-2008 Thomas Perl and the gPodder Team'
117 app_website = 'http://www.gpodder.org/'
119 # these will be filled with pathnames in bin/gpodder
120 glade_dir = [ 'share', 'gpodder' ]
121 icon_dir = [ 'share', 'pixmaps', 'gpodder.png' ]
122 scalable_dir = [ 'share', 'icons', 'hicolor', 'scalable', 'apps', 'gpodder.svg' ]
125 class GladeWidget(SimpleGladeApp.SimpleGladeApp):
126 gpodder_main_window = None
127 finger_friendly_widgets = []
129 def __init__( self, **kwargs):
130 path = os.path.join( glade_dir, '%s.glade' % app_name)
131 root = self.__class__.__name__
132 domain = app_name
134 SimpleGladeApp.SimpleGladeApp.__init__( self, path, root, domain, **kwargs)
136 # Set widgets to finger-friendly mode if on Maemo
137 for widget_name in self.finger_friendly_widgets:
138 if hasattr(self, widget_name):
139 self.set_finger_friendly(getattr(self, widget_name))
140 else:
141 log('Finger-friendly widget not found: %s', widget_name, sender=self)
143 if root == 'gPodder':
144 GladeWidget.gpodder_main_window = self.gPodder
145 else:
146 # If we have a child window, set it transient for our main window
147 getattr( self, root).set_transient_for( GladeWidget.gpodder_main_window)
149 if gpodder.interface == gpodder.GUI:
150 if hasattr( self, 'center_on_widget'):
151 ( x, y ) = self.gpodder_main_window.get_position()
152 a = self.center_on_widget.allocation
153 ( x, y ) = ( x + a.x, y + a.y )
154 ( w, h ) = ( a.width, a.height )
155 ( pw, ph ) = getattr( self, root).get_size()
156 getattr( self, root).move( x + w/2 - pw/2, y + h/2 - ph/2)
157 else:
158 getattr( self, root).set_position( gtk.WIN_POS_CENTER_ON_PARENT)
160 def notification(self, message, title=None):
161 util.idle_add(self.show_message, message, title)
163 def show_message( self, message, title = None):
164 if hasattr(self, 'tray_icon') and hasattr(self, 'minimized') and self.tray_icon and self.minimized:
165 if title is None:
166 title = 'gPodder'
167 self.tray_icon.send_notification(message, title)
168 return
170 if gpodder.interface == gpodder.GUI:
171 dlg = gtk.MessageDialog(GladeWidget.gpodder_main_window, gtk.DIALOG_MODAL, gtk.MESSAGE_INFO, gtk.BUTTONS_OK)
172 if title:
173 dlg.set_title(str(title))
174 dlg.set_markup('<span weight="bold" size="larger">%s</span>\n\n%s' % (title, message))
175 else:
176 dlg.set_markup('<span weight="bold" size="larger">%s</span>' % (message))
177 elif gpodder.interface == gpodder.MAEMO:
178 dlg = hildon.Note('information', (GladeWidget.gpodder_main_window, message))
180 dlg.run()
181 dlg.destroy()
183 def set_finger_friendly(self, widget):
185 If we are on Maemo, we carry out the necessary
186 operations to turn a widget into a finger-friendly
187 one, depending on which type of widget it is (i.e.
188 buttons will have more padding, TreeViews a thick
189 scrollbar, etc..)
191 if gpodder.interface == gpodder.MAEMO:
192 if isinstance(widget, gtk.Misc):
193 widget.set_padding(0, 5)
194 elif isinstance(widget, gtk.Button):
195 for child in widget.get_children():
196 if isinstance(child, gtk.Alignment):
197 child.set_padding(5, 5, 5, 5)
198 else:
199 child.set_padding(5, 5)
200 elif isinstance(widget, gtk.TreeView) or isinstance(widget, gtk.TextView):
201 parent = widget.get_parent()
202 if isinstance(parent, gtk.ScrolledWindow):
203 hildon.hildon_helper_set_thumb_scrollbar(parent, True)
204 elif isinstance(widget, gtk.MenuItem):
205 for child in widget.get_children():
206 self.set_finger_friendly(child)
207 submenu = widget.get_submenu()
208 if submenu is not None:
209 for child in submenu.get_children():
210 self.set_finger_friendly(child)
211 elif isinstance(widget, gtk.Menu):
212 for child in widget.get_children():
213 self.set_finger_friendly(child)
214 else:
215 log('Cannot set widget finger-friendly: %s', widget, sender=self)
217 return widget
219 def show_confirmation( self, message, title = None):
220 if gpodder.interface == gpodder.GUI:
221 affirmative = gtk.RESPONSE_YES
222 dlg = gtk.MessageDialog(GladeWidget.gpodder_main_window, gtk.DIALOG_MODAL, gtk.MESSAGE_QUESTION, gtk.BUTTONS_YES_NO)
223 if title:
224 dlg.set_title(str(title))
225 dlg.set_markup('<span weight="bold" size="larger">%s</span>\n\n%s' % (title, message))
226 else:
227 dlg.set_markup('<span weight="bold" size="larger">%s</span>' % (message))
228 elif gpodder.interface == gpodder.MAEMO:
229 affirmative = gtk.RESPONSE_OK
230 dlg = hildon.Note('confirmation', (GladeWidget.gpodder_main_window, message))
232 response = dlg.run()
233 dlg.destroy()
235 return response == affirmative
237 def UsernamePasswordDialog( self, title, message, username=None, password=None, username_prompt=_('Username'), register_callback=None):
238 """ An authentication dialog based on
239 http://ardoris.wordpress.com/2008/07/05/pygtk-text-entry-dialog/ """
241 dialog = gtk.MessageDialog(
242 GladeWidget.gpodder_main_window,
243 gtk.DIALOG_MODAL | gtk.DIALOG_DESTROY_WITH_PARENT,
244 gtk.MESSAGE_QUESTION,
245 gtk.BUTTONS_OK_CANCEL )
247 dialog.set_image(gtk.image_new_from_stock(gtk.STOCK_DIALOG_AUTHENTICATION, gtk.ICON_SIZE_DIALOG))
249 dialog.set_markup('<span weight="bold" size="larger">' + title + '</span>')
250 dialog.set_title(_('Authentication required'))
251 dialog.format_secondary_markup(message)
252 dialog.set_default_response(gtk.RESPONSE_OK)
254 if register_callback is not None:
255 dialog.add_button(_('New user'), gtk.RESPONSE_HELP)
257 username_entry = gtk.Entry()
258 password_entry = gtk.Entry()
260 username_entry.connect('activate', lambda w: password_entry.grab_focus())
261 password_entry.set_visibility(False)
262 password_entry.set_activates_default(True)
264 if username is not None:
265 username_entry.set_text(username)
266 if password is not None:
267 password_entry.set_text(password)
269 table = gtk.Table(2, 2)
270 table.set_row_spacings(6)
271 table.set_col_spacings(6)
273 username_label = gtk.Label()
274 username_label.set_markup('<b>' + username_prompt + ':</b>')
275 username_label.set_alignment(0.0, 0.5)
276 table.attach(username_label, 0, 1, 0, 1, gtk.FILL, 0)
277 table.attach(username_entry, 1, 2, 0, 1)
279 password_label = gtk.Label()
280 password_label.set_markup('<b>' + _('Password') + ':</b>')
281 password_label.set_alignment(0.0, 0.5)
282 table.attach(password_label, 0, 1, 1, 2, gtk.FILL, 0)
283 table.attach(password_entry, 1, 2, 1, 2)
285 dialog.vbox.pack_end(table, True, True, 0)
286 dialog.show_all()
287 response = dialog.run()
289 while response == gtk.RESPONSE_HELP:
290 register_callback()
291 response = dialog.run()
293 password_entry.set_visibility(True)
294 dialog.destroy()
296 return response == gtk.RESPONSE_OK, ( username_entry.get_text(), password_entry.get_text() )
298 def show_copy_dialog( self, src_filename, dst_filename = None, dst_directory = None, title = _('Select destination')):
299 if dst_filename is None:
300 dst_filename = src_filename
302 if dst_directory is None:
303 dst_directory = os.path.expanduser( '~')
305 ( base, extension ) = os.path.splitext( src_filename)
307 if not dst_filename.endswith( extension):
308 dst_filename += extension
310 if gpodder.interface == gpodder.GUI:
311 dlg = gtk.FileChooserDialog(title=title, parent=GladeWidget.gpodder_main_window, action=gtk.FILE_CHOOSER_ACTION_SAVE)
312 dlg.add_button(gtk.STOCK_CANCEL, gtk.RESPONSE_CANCEL)
313 dlg.add_button(gtk.STOCK_SAVE, gtk.RESPONSE_OK)
314 elif gpodder.interface == gpodder.MAEMO:
315 dlg = hildon.FileChooserDialog(GladeWidget.gpodder_main_window, gtk.FILE_CHOOSER_ACTION_SAVE)
317 dlg.set_do_overwrite_confirmation( True)
318 dlg.set_current_name( os.path.basename( dst_filename))
319 dlg.set_current_folder( dst_directory)
321 result = False
322 folder = dst_directory
323 if dlg.run() == gtk.RESPONSE_OK:
324 result = True
325 dst_filename = dlg.get_filename()
326 folder = dlg.get_current_folder()
327 if not dst_filename.endswith( extension):
328 dst_filename += extension
330 log( 'Copying %s => %s', src_filename, dst_filename, sender = self)
332 try:
333 shutil.copyfile( src_filename, dst_filename)
334 except:
335 log( 'Error copying file.', sender = self, traceback = True)
337 dlg.destroy()
338 return (result, folder)
341 class gPodder(GladeWidget):
342 finger_friendly_widgets = ['btnCancelFeedUpdate', 'label2', 'labelDownloads', 'itemQuit', 'menuPodcasts', 'advanced1', 'menuChannels', 'menuHelp']
343 ENTER_URL_TEXT = _('Enter podcast URL...')
345 def new(self):
346 if gpodder.interface == gpodder.MAEMO:
347 # Maemo-specific changes to the UI
348 global scalable_dir
349 scalable_dir = scalable_dir.replace('.svg', '.png')
351 self.app = hildon.Program()
352 gtk.set_application_name('gPodder')
353 self.window = hildon.Window()
354 self.window.connect('delete-event', self.on_gPodder_delete_event)
355 self.window.connect('window-state-event', self.window_state_event)
357 self.itemUpdateChannel.show()
358 self.UpdateChannelSeparator.show()
360 # Give toolbar to the hildon window
361 self.toolbar.parent.remove(self.toolbar)
362 self.window.add_toolbar(self.toolbar)
364 # START TEMPORARY FIX FOR TOOLBAR STYLE
365 # It seems like libglade for python still mixes
366 # old GtkToolbar API with new ones - maybe this
367 # is the reason why setting the style doesn't
368 # work very well. This small hack fixes that :)
369 self.toolbar.set_style(gtk.TOOLBAR_BOTH_HORIZ)
370 def remove_label(w):
371 if hasattr(w, 'set_label'):
372 w.set_label(None)
373 self.toolbar.foreach(remove_label)
374 # END TEMPORARY FIX FOR TOOLBAR STYLE
376 self.app.add_window(self.window)
377 self.vMain.reparent(self.window)
378 self.gPodder = self.window
380 # Reparent the main menu
381 menu = gtk.Menu()
382 for child in self.mainMenu.get_children():
383 child.reparent(menu)
384 self.itemQuit.reparent(menu)
385 self.window.set_menu(menu)
387 self.mainMenu.destroy()
388 self.window.show()
390 # do some widget hiding
391 self.toolbar.remove(self.toolTransfer)
392 self.itemTransferSelected.hide_all()
393 self.item_email_subscriptions.hide_all()
395 # Feed cache update button
396 self.label120.set_text(_('Update'))
398 # get screen real estate
399 self.hboxContainer.set_border_width(0)
401 # Offer importing of videocenter podcasts
402 if os.path.exists(os.path.expanduser('~/videocenter')):
403 self.item_upgrade_from_videocenter.show()
404 self.upgrade_from_videocenter_separator.show()
406 self.gPodder.connect('key-press-event', self.on_key_press)
407 self.treeChannels.connect('size-allocate', self.on_tree_channels_resize)
409 if gl.config.show_url_entry_in_podcast_list:
410 self.hboxAddChannel.show()
412 if not gl.config.show_toolbar:
413 self.toolbar.hide()
415 gl.config.add_observer(self.on_config_changed)
416 self.default_entry_text_color = self.entryAddChannel.get_style().text[gtk.STATE_NORMAL]
417 self.entryAddChannel.connect('focus-in-event', self.entry_add_channel_focus)
418 self.entryAddChannel.connect('focus-out-event', self.entry_add_channel_unfocus)
419 self.entry_add_channel_unfocus(self.entryAddChannel, None)
421 self.uar = None
422 self.tray_icon = None
423 self.gpodder_episode_window = None
425 self.fullscreen = False
426 self.minimized = False
427 self.gPodder.connect('window-state-event', self.window_state_event)
429 self.already_notified_new_episodes = []
430 self.show_hide_tray_icon()
431 self.no_episode_selected.set_sensitive(False)
433 self.itemShowToolbar.set_active(gl.config.show_toolbar)
434 self.itemShowDescription.set_active(gl.config.episode_list_descriptions)
436 gl.config.connect_gtk_window(self.gPodder, 'main_window')
437 gl.config.connect_gtk_paned( 'paned_position', self.channelPaned)
439 gl.config.connect_gtk_spinbutton('max_downloads', self.spinMaxDownloads)
440 gl.config.connect_gtk_togglebutton('max_downloads_enabled', self.cbMaxDownloads)
441 gl.config.connect_gtk_spinbutton('limit_rate_value', self.spinLimitDownloads)
442 gl.config.connect_gtk_togglebutton('limit_rate', self.cbLimitDownloads)
444 # Make sure we free/close the download queue when we
445 # update the "max downloads" spin button
446 changed_cb = lambda spinbutton: services.download_status_manager.update_max_downloads()
447 self.spinMaxDownloads.connect('value-changed', changed_cb)
449 self.default_title = None
450 if app_version.rfind('git') != -1:
451 self.set_title('gPodder %s' % app_version)
452 else:
453 title = self.gPodder.get_title()
454 if title is not None:
455 self.set_title(title)
456 else:
457 self.set_title(_('gPodder'))
459 gtk.about_dialog_set_url_hook(lambda dlg, link, data: util.open_website(link), None)
461 # cell renderers for channel tree
462 iconcolumn = gtk.TreeViewColumn('')
464 iconcell = gtk.CellRendererPixbuf()
465 iconcolumn.pack_start( iconcell, False)
466 iconcolumn.add_attribute( iconcell, 'pixbuf', 5)
467 self.cell_channel_icon = iconcell
469 namecolumn = gtk.TreeViewColumn('')
470 namecell = gtk.CellRendererText()
471 namecell.set_property('foreground-set', True)
472 namecell.set_property('ellipsize', pango.ELLIPSIZE_END)
473 namecolumn.pack_start( namecell, True)
474 namecolumn.add_attribute( namecell, 'markup', 2)
475 namecolumn.add_attribute( namecell, 'foreground', 8)
477 iconcell = gtk.CellRendererPixbuf()
478 iconcell.set_property('xalign', 1.0)
479 namecolumn.pack_start( iconcell, False)
480 namecolumn.add_attribute( iconcell, 'pixbuf', 3)
481 namecolumn.add_attribute(iconcell, 'visible', 7)
482 self.cell_channel_pill = iconcell
484 self.treeChannels.append_column(iconcolumn)
485 self.treeChannels.append_column(namecolumn)
486 self.treeChannels.set_headers_visible(False)
488 # enable alternating colors hint
489 self.treeAvailable.set_rules_hint( True)
490 self.treeChannels.set_rules_hint( True)
492 # connect to tooltip signals
493 try:
494 self.treeChannels.set_property('has-tooltip', True)
495 self.treeChannels.connect('query-tooltip', self.treeview_channels_query_tooltip)
496 self.treeAvailable.set_property('has-tooltip', True)
497 self.treeAvailable.connect('query-tooltip', self.treeview_episodes_query_tooltip)
498 except:
499 log('I cannot set has-tooltip/query-tooltip (need at least PyGTK 2.12)', sender = self)
500 self.last_tooltip_channel = None
501 self.last_tooltip_episode = None
502 self.podcast_list_can_tooltip = True
503 self.episode_list_can_tooltip = True
505 # Add our context menu to treeAvailable
506 if gpodder.interface == gpodder.MAEMO:
507 self.treeAvailable.connect('button-release-event', self.treeview_button_pressed)
508 else:
509 self.treeAvailable.connect('button-press-event', self.treeview_button_pressed)
510 self.treeChannels.connect('button-press-event', self.treeview_channels_button_pressed)
512 iconcell = gtk.CellRendererPixbuf()
513 if gpodder.interface == gpodder.MAEMO:
514 status_column_label = ''
515 else:
516 status_column_label = _('Status')
517 iconcolumn = gtk.TreeViewColumn(status_column_label, iconcell, pixbuf=4)
519 namecell = gtk.CellRendererText()
520 namecell.set_property('ellipsize', pango.ELLIPSIZE_END)
521 namecolumn = gtk.TreeViewColumn(_("Episode"), namecell, markup=6)
522 namecolumn.set_sizing(gtk.TREE_VIEW_COLUMN_AUTOSIZE)
523 namecolumn.set_expand(True)
525 sizecell = gtk.CellRendererText()
526 sizecolumn = gtk.TreeViewColumn( _("Size"), sizecell, text=2)
528 releasecell = gtk.CellRendererText()
529 releasecolumn = gtk.TreeViewColumn( _("Released"), releasecell, text=5)
531 for itemcolumn in (iconcolumn, namecolumn, sizecolumn, releasecolumn):
532 itemcolumn.set_reorderable(True)
533 self.treeAvailable.append_column(itemcolumn)
535 if gpodder.interface == gpodder.MAEMO:
536 # Due to screen space contraints, we
537 # hide these columns here by default
538 self.column_size = sizecolumn
539 self.column_released = releasecolumn
540 self.column_released.set_visible(False)
541 self.column_size.set_visible(False)
543 # enable search in treeavailable
544 self.treeAvailable.set_search_equal_func( self.treeAvailable_search_equal)
546 # enable multiple selection support
547 self.treeAvailable.get_selection().set_mode( gtk.SELECTION_MULTIPLE)
548 self.treeDownloads.get_selection().set_mode( gtk.SELECTION_MULTIPLE)
550 # columns and renderers for "download progress" tab
551 episodecell = gtk.CellRendererText()
552 episodecell.set_property('ellipsize', pango.ELLIPSIZE_END)
553 episodecolumn = gtk.TreeViewColumn( _("Episode"), episodecell, text=0)
554 episodecolumn.set_sizing(gtk.TREE_VIEW_COLUMN_AUTOSIZE)
555 episodecolumn.set_expand(True)
557 speedcell = gtk.CellRendererText()
558 speedcolumn = gtk.TreeViewColumn( _("Speed"), speedcell, text=1)
560 progresscell = gtk.CellRendererProgress()
561 progresscolumn = gtk.TreeViewColumn( _("Progress"), progresscell, value=2)
562 progresscolumn.set_expand(True)
564 for itemcolumn in ( episodecolumn, speedcolumn, progresscolumn ):
565 self.treeDownloads.append_column( itemcolumn)
567 # After we've set up most of the window, show it :)
568 if not gpodder.interface == gpodder.MAEMO:
569 self.gPodder.show()
571 if self.tray_icon:
572 if gl.config.start_iconified:
573 self.iconify_main_window()
574 elif gl.config.minimize_to_tray:
575 self.tray_icon.set_visible(False)
577 # a dictionary that maps episode URLs to the current
578 # treeAvailable row numbers to generate tree paths
579 self.url_path_mapping = {}
581 # a dictionary that maps channel URLs to the current
582 # treeChannels row numbers to generate tree paths
583 self.channel_url_path_mapping = {}
585 services.download_status_manager.register( 'list-changed', self.download_status_updated)
586 services.download_status_manager.register( 'progress-changed', self.download_progress_updated)
587 services.cover_downloader.register('cover-available', self.cover_download_finished)
588 services.cover_downloader.register('cover-removed', self.cover_file_removed)
589 self.cover_cache = {}
591 self.treeDownloads.set_model( services.download_status_manager.tree_model)
593 #Add Drag and Drop Support
594 flags = gtk.DEST_DEFAULT_ALL
595 targets = [ ('text/plain', 0, 2), ('STRING', 0, 3), ('TEXT', 0, 4) ]
596 actions = gtk.gdk.ACTION_DEFAULT | gtk.gdk.ACTION_COPY
597 self.treeChannels.drag_dest_set( flags, targets, actions)
598 self.treeChannels.connect( 'drag_data_received', self.drag_data_received)
600 # Subscribed channels
601 self.active_channel = None
602 self.channels = load_channels()
603 self.channel_list_changed = True
604 self.update_podcasts_tab()
606 # load list of user applications for audio playback
607 self.user_apps_reader = UserAppsReader(['audio', 'video'])
608 Thread(target=self.read_apps).start()
610 # Clean up old, orphaned download files
611 gl.clean_up_downloads( delete_partial = True)
613 # Set the "Device" menu item for the first time
614 self.update_item_device()
616 # Last folder used for saving episodes
617 self.folder_for_saving_episodes = None
619 # Set up default channel colors
620 self.channel_colors = {
621 'default': None,
622 'updating': gl.config.color_updating_feeds,
623 'parse_error': '#ff0000',
626 # Now, update the feed cache, when everything's in place
627 self.btnUpdateFeeds.show_all()
628 self.updated_feeds = 0
629 self.updating_feed_cache = False
630 self.feed_cache_update_cancelled = False
631 self.update_feed_cache(force_update=gl.config.update_on_startup)
633 # Start the auto-update procedure
634 self.auto_update_procedure(first_run=True)
636 # Delete old episodes if the user wishes to
637 if gl.config.auto_remove_old_episodes:
638 old_episodes = self.get_old_episodes()
639 if len(old_episodes) > 0:
640 self.delete_episode_list(old_episodes, confirm=False)
641 self.updateComboBox()
643 # First-time users should be asked if they want to see the OPML
644 if len(self.channels) == 0:
645 util.idle_add(self.on_itemUpdate_activate, None)
647 def on_tree_channels_resize(self, widget, allocation):
648 if not gl.config.podcast_sidebar_save_space:
649 return
651 window_allocation = self.gPodder.get_allocation()
652 percentage = 100. * float(allocation.width) / float(window_allocation.width)
653 if hasattr(self, 'cell_channel_icon'):
654 self.cell_channel_icon.set_property('visible', bool(percentage > 22.))
655 if hasattr(self, 'cell_channel_pill'):
656 self.cell_channel_pill.set_property('visible', bool(percentage > 25.))
658 def entry_add_channel_focus(self, widget, event):
659 widget.modify_text(gtk.STATE_NORMAL, self.default_entry_text_color)
660 if widget.get_text() == self.ENTER_URL_TEXT:
661 widget.set_text('')
663 def entry_add_channel_unfocus(self, widget, event):
664 if widget.get_text() == '':
665 widget.set_text(self.ENTER_URL_TEXT)
666 widget.modify_text(gtk.STATE_NORMAL, gtk.gdk.color_parse('#aaaaaa'))
668 def on_config_changed(self, name, old_value, new_value):
669 if name == 'show_toolbar':
670 if new_value:
671 self.toolbar.show()
672 else:
673 self.toolbar.hide()
674 elif name == 'episode_list_descriptions':
675 self.updateTreeView()
676 elif name == 'show_url_entry_in_podcast_list':
677 if new_value:
678 self.hboxAddChannel.show()
679 else:
680 self.hboxAddChannel.hide()
682 def read_apps(self):
683 time.sleep(3) # give other parts of gpodder a chance to start up
684 self.user_apps_reader.read()
685 util.idle_add(self.user_apps_reader.get_applications_as_model, 'audio', False)
686 util.idle_add(self.user_apps_reader.get_applications_as_model, 'video', False)
688 def treeview_episodes_query_tooltip(self, treeview, x, y, keyboard_tooltip, tooltip):
689 # With get_bin_window, we get the window that contains the rows without
690 # the header. The Y coordinate of this window will be the height of the
691 # treeview header. This is the amount we have to subtract from the
692 # event's Y coordinate to get the coordinate to pass to get_path_at_pos
693 (x_bin, y_bin) = treeview.get_bin_window().get_position()
694 y -= x_bin
695 y -= y_bin
696 (path, column, rx, ry) = treeview.get_path_at_pos( x, y) or (None,)*4
698 if not self.episode_list_can_tooltip or (column is not None and column != treeview.get_columns()[0]):
699 self.last_tooltip_episode = None
700 return False
702 if path is not None:
703 model = treeview.get_model()
704 iter = model.get_iter(path)
705 url = model.get_value(iter, 0)
706 description = model.get_value(iter, 7)
707 if self.last_tooltip_episode is not None and self.last_tooltip_episode != url:
708 self.last_tooltip_episode = None
709 return False
710 self.last_tooltip_episode = url
712 if len(description) > 400:
713 description = description[:398]+'[...]'
715 tooltip.set_text(description)
716 return True
718 self.last_tooltip_episode = None
719 return False
721 def podcast_list_allow_tooltips(self):
722 self.podcast_list_can_tooltip = True
724 def episode_list_allow_tooltips(self):
725 self.episode_list_can_tooltip = True
727 def treeview_channels_query_tooltip(self, treeview, x, y, keyboard_tooltip, tooltip):
728 (path, column, rx, ry) = treeview.get_path_at_pos( x, y) or (None,)*4
730 if not self.podcast_list_can_tooltip or (column is not None and column != treeview.get_columns()[0]):
731 self.last_tooltip_channel = None
732 return False
734 if path is not None:
735 model = treeview.get_model()
736 iter = model.get_iter(path)
737 url = model.get_value(iter, 0)
738 for channel in self.channels:
739 if channel.url == url:
740 if self.last_tooltip_channel is not None and self.last_tooltip_channel != channel:
741 self.last_tooltip_channel = None
742 return False
743 self.last_tooltip_channel = channel
744 channel.request_save_dir_size()
745 diskspace_str = gl.format_filesize(channel.save_dir_size, 0)
746 error_str = model.get_value(iter, 6)
747 if error_str:
748 error_str = _('Feedparser error: %s') % saxutils.escape(error_str.strip())
749 error_str = '<span foreground="#ff0000">%s</span>' % error_str
750 table = gtk.Table(rows=3, columns=3)
751 table.set_row_spacings(5)
752 table.set_col_spacings(5)
753 table.set_border_width(5)
755 heading = gtk.Label()
756 heading.set_alignment(0, 1)
757 heading.set_markup('<b><big>%s</big></b>\n<small>%s</small>' % (saxutils.escape(channel.title), saxutils.escape(channel.url)))
758 table.attach(heading, 0, 1, 0, 1)
759 size_info = gtk.Label()
760 size_info.set_alignment(1, 1)
761 size_info.set_justify(gtk.JUSTIFY_RIGHT)
762 size_info.set_markup('<b>%s</b>\n<small>%s</small>' % (diskspace_str, _('disk usage')))
763 table.attach(size_info, 2, 3, 0, 1)
765 table.attach(gtk.HSeparator(), 0, 3, 1, 2)
767 if len(channel.description) < 500:
768 description = channel.description
769 else:
770 pos = channel.description.find('\n\n')
771 if pos == -1 or pos > 500:
772 description = channel.description[:498]+'[...]'
773 else:
774 description = channel.description[:pos]
776 description = gtk.Label(description)
777 if error_str:
778 description.set_markup(error_str)
779 description.set_alignment(0, 0)
780 description.set_line_wrap(True)
781 table.attach(description, 0, 3, 2, 3)
783 table.show_all()
784 tooltip.set_custom(table)
786 return True
788 self.last_tooltip_channel = None
789 return False
791 def update_m3u_playlist_clicked(self, widget):
792 self.active_channel.update_m3u_playlist()
793 self.show_message(_('Updated M3U playlist in download folder.'), _('Updated playlist'))
795 def treeview_channels_button_pressed( self, treeview, event):
796 global WEB_BROWSER_ICON
798 if event.button == 3:
799 ( x, y ) = ( int(event.x), int(event.y) )
800 ( path, column, rx, ry ) = treeview.get_path_at_pos( x, y) or (None,)*4
802 paths = []
804 # Did the user right-click into a selection?
805 selection = treeview.get_selection()
806 if selection.count_selected_rows() and path:
807 ( model, paths ) = selection.get_selected_rows()
808 if path not in paths:
809 # We have right-clicked, but not into the
810 # selection, assume we don't want to operate
811 # on the selection
812 paths = []
814 # No selection or right click not in selection:
815 # Select the single item where we clicked
816 if not len( paths) and path:
817 treeview.grab_focus()
818 treeview.set_cursor( path, column, 0)
820 ( model, paths ) = ( treeview.get_model(), [ path ] )
822 # We did not find a selection, and the user didn't
823 # click on an item to select -- don't show the menu
824 if not len( paths):
825 return True
827 menu = gtk.Menu()
829 item = gtk.ImageMenuItem( _('Open download folder'))
830 item.set_image( gtk.image_new_from_icon_name( 'folder-open', gtk.ICON_SIZE_MENU))
831 item.connect('activate', lambda x: util.gui_open(self.active_channel.save_dir))
832 menu.append( item)
834 item = gtk.ImageMenuItem( _('Update Feed'))
835 item.set_image( gtk.image_new_from_icon_name( 'gtk-refresh', gtk.ICON_SIZE_MENU))
836 item.connect('activate', self.on_itemUpdateChannel_activate )
837 item.set_sensitive( not self.updating_feed_cache )
838 menu.append( item)
840 if gl.config.create_m3u_playlists:
841 item = gtk.ImageMenuItem(_('Update M3U playlist'))
842 item.set_image(gtk.image_new_from_stock(gtk.STOCK_REFRESH, gtk.ICON_SIZE_MENU))
843 item.connect('activate', self.update_m3u_playlist_clicked)
844 menu.append(item)
846 if self.active_channel.link:
847 item = gtk.ImageMenuItem(_('Visit website'))
848 item.set_image(gtk.image_new_from_icon_name(WEB_BROWSER_ICON, gtk.ICON_SIZE_MENU))
849 item.connect('activate', lambda w: util.open_website(self.active_channel.link))
850 menu.append(item)
852 if self.active_channel.channel_is_locked:
853 item = gtk.ImageMenuItem(_('Allow deletion of all episodes'))
854 item.set_image(gtk.image_new_from_stock(gtk.STOCK_DIALOG_AUTHENTICATION, gtk.ICON_SIZE_MENU))
855 item.connect('activate', self.on_channel_toggle_lock_activate)
856 menu.append(self.set_finger_friendly(item))
857 else:
858 item = gtk.ImageMenuItem(_('Prohibit deletion of all episodes'))
859 item.set_image(gtk.image_new_from_stock(gtk.STOCK_DIALOG_AUTHENTICATION, gtk.ICON_SIZE_MENU))
860 item.connect('activate', self.on_channel_toggle_lock_activate)
861 menu.append(self.set_finger_friendly(item))
864 menu.append( gtk.SeparatorMenuItem())
866 item = gtk.ImageMenuItem(gtk.STOCK_EDIT)
867 item.connect( 'activate', self.on_itemEditChannel_activate)
868 menu.append( item)
870 item = gtk.ImageMenuItem(gtk.STOCK_DELETE)
871 item.connect( 'activate', self.on_itemRemoveChannel_activate)
872 menu.append( item)
874 menu.show_all()
875 # Disable tooltips while we are showing the menu, so
876 # the tooltip will not appear over the menu
877 self.podcast_list_can_tooltip = False
878 menu.connect('deactivate', lambda menushell: self.podcast_list_allow_tooltips())
879 menu.popup( None, None, None, event.button, event.time)
881 return True
883 def on_itemClose_activate(self, widget):
884 if self.tray_icon is not None:
885 if gpodder.interface == gpodder.MAEMO:
886 self.gPodder.set_property('visible', False)
887 else:
888 self.iconify_main_window()
889 else:
890 self.on_gPodder_delete_event(widget)
892 def cover_file_removed(self, channel_url):
894 The Cover Downloader calls this when a previously-
895 available cover has been removed from the disk. We
896 have to update our cache to reflect this change.
898 (COLUMN_URL, COLUMN_PIXBUF) = (0, 5)
899 for row in self.treeChannels.get_model():
900 if row[COLUMN_URL] == channel_url:
901 row[COLUMN_PIXBUF] = None
902 key = (channel_url, gl.config.podcast_list_icon_size, \
903 gl.config.podcast_list_icon_size)
904 if key in self.cover_cache:
905 del self.cover_cache[key]
908 def cover_download_finished(self, channel_url, pixbuf):
910 The Cover Downloader calls this when it has finished
911 downloading (or registering, if already downloaded)
912 a new channel cover, which is ready for displaying.
914 if pixbuf is not None:
915 (COLUMN_URL, COLUMN_PIXBUF) = (0, 5)
916 for row in self.treeChannels.get_model():
917 if row[COLUMN_URL] == channel_url and row[COLUMN_PIXBUF] is None:
918 new_pixbuf = util.resize_pixbuf_keep_ratio(pixbuf, gl.config.podcast_list_icon_size, gl.config.podcast_list_icon_size, channel_url, self.cover_cache)
919 row[COLUMN_PIXBUF] = new_pixbuf or pixbuf
921 def save_episode_as_file( self, url, *args):
922 episode = self.active_channel.find_episode(url)
924 folder = self.folder_for_saving_episodes
925 (result, folder) = self.show_copy_dialog(src_filename=episode.local_filename(), dst_filename=episode.sync_filename(), dst_directory=folder)
926 self.folder_for_saving_episodes = folder
928 def copy_episode_bluetooth(self, url, *args):
929 episode = self.active_channel.find_episode(url)
930 filename = episode.local_filename()
932 if gl.config.bluetooth_use_device_address:
933 device = gl.config.bluetooth_device_address
934 else:
935 device = None
937 destfile = os.path.join(gl.tempdir, util.sanitize_filename(episode.sync_filename()))
938 (base, ext) = os.path.splitext(filename)
939 if not destfile.endswith(ext):
940 destfile += ext
942 if gl.config.bluetooth_use_converter:
943 title = _('Converting file')
944 message = _('Please wait while gPodder converts your media file for bluetooth file transfer.')
945 dlg = gtk.MessageDialog(self.gPodder, gtk.DIALOG_MODAL, gtk.MESSAGE_INFO, gtk.BUTTONS_NONE)
946 dlg.set_title(title)
947 dlg.set_markup('<span weight="bold" size="larger">%s</span>\n\n%s'%(title, message))
948 dlg.show_all()
949 else:
950 dlg = None
952 def convert_and_send_thread(filename, destfile, device, dialog, notify):
953 if gl.config.bluetooth_use_converter:
954 p = subprocess.Popen([gl.config.bluetooth_converter, filename, destfile], stdout=sys.stdout, stderr=sys.stderr)
955 result = p.wait()
956 if dialog is not None:
957 dialog.destroy()
958 else:
959 try:
960 shutil.copyfile(filename, destfile)
961 result = 0
962 except:
963 log('Cannot copy "%s" to "%s".', filename, destfile, sender=self)
964 result = 1
966 if result == 0 or not os.path.exists(destfile):
967 util.bluetooth_send_file(destfile, device)
968 else:
969 notify(_('Error converting file.'), _('Bluetooth file transfer'))
970 util.delete_file(destfile)
972 Thread(target=convert_and_send_thread, args=[filename, destfile, device, dlg, self.notification]).start()
974 def treeview_button_pressed( self, treeview, event):
975 global WEB_BROWSER_ICON
977 # Use right-click for the Desktop version and left-click for Maemo
978 if (event.button == 1 and gpodder.interface == gpodder.MAEMO) or \
979 (event.button == 3 and gpodder.interface == gpodder.GUI):
980 ( x, y ) = ( int(event.x), int(event.y) )
981 ( path, column, rx, ry ) = treeview.get_path_at_pos( x, y) or (None,)*4
983 paths = []
985 # Did the user right-click into a selection?
986 selection = self.treeAvailable.get_selection()
987 if selection.count_selected_rows() and path:
988 ( model, paths ) = selection.get_selected_rows()
989 if path not in paths:
990 # We have right-clicked, but not into the
991 # selection, assume we don't want to operate
992 # on the selection
993 paths = []
995 # No selection or right click not in selection:
996 # Select the single item where we clicked
997 if not len( paths) and path:
998 treeview.grab_focus()
999 treeview.set_cursor( path, column, 0)
1001 ( model, paths ) = ( treeview.get_model(), [ path ] )
1003 # We did not find a selection, and the user didn't
1004 # click on an item to select -- don't show the menu
1005 if not len( paths):
1006 return True
1008 first_url = model.get_value( model.get_iter( paths[0]), 0)
1009 episode = db.load_episode(first_url)
1011 menu = gtk.Menu()
1013 (can_play, can_download, can_transfer, can_cancel, can_delete, open_instead_of_play) = self.play_or_download()
1015 if can_play:
1016 if open_instead_of_play:
1017 item = gtk.ImageMenuItem(gtk.STOCK_OPEN)
1018 else:
1019 item = gtk.ImageMenuItem(gtk.STOCK_MEDIA_PLAY)
1020 item.connect( 'activate', lambda w: self.on_treeAvailable_row_activated( self.toolPlay))
1021 menu.append(self.set_finger_friendly(item))
1023 if not episode['is_locked'] and can_delete:
1024 item = gtk.ImageMenuItem(gtk.STOCK_DELETE)
1025 item.connect('activate', self.on_btnDownloadedDelete_clicked)
1026 menu.append(self.set_finger_friendly(item))
1028 if can_cancel:
1029 item = gtk.ImageMenuItem( _('Cancel download'))
1030 item.set_image( gtk.image_new_from_stock( gtk.STOCK_STOP, gtk.ICON_SIZE_MENU))
1031 item.connect( 'activate', lambda w: self.on_treeDownloads_row_activated( self.toolCancel))
1032 menu.append(self.set_finger_friendly(item))
1034 if can_download:
1035 item = gtk.ImageMenuItem(_('Download'))
1036 item.set_image( gtk.image_new_from_stock( gtk.STOCK_GO_DOWN, gtk.ICON_SIZE_MENU))
1037 item.connect( 'activate', lambda w: self.on_treeAvailable_row_activated( self.toolDownload))
1038 menu.append(self.set_finger_friendly(item))
1040 if episode['state'] == db.STATE_NORMAL and not episode['is_played']: # can_download:
1041 item = gtk.ImageMenuItem(_('Do not download'))
1042 item.set_image(gtk.image_new_from_stock(gtk.STOCK_DELETE, gtk.ICON_SIZE_MENU))
1043 item.connect('activate', lambda w: self.mark_selected_episodes_old())
1044 menu.append(self.set_finger_friendly(item))
1045 elif episode['state'] == db.STATE_NORMAL and can_download:
1046 item = gtk.ImageMenuItem(_('Mark as new'))
1047 item.set_image(gtk.image_new_from_stock(gtk.STOCK_ABOUT, gtk.ICON_SIZE_MENU))
1048 item.connect('activate', lambda w: self.mark_selected_episodes_new())
1049 menu.append(self.set_finger_friendly(item))
1051 if can_play and not can_download:
1052 menu.append( gtk.SeparatorMenuItem())
1053 item = gtk.ImageMenuItem(_('Save to disk'))
1054 item.set_image(gtk.image_new_from_stock(gtk.STOCK_SAVE_AS, gtk.ICON_SIZE_MENU))
1055 item.connect( 'activate', lambda w: self.for_each_selected_episode_url(self.save_episode_as_file))
1056 menu.append(self.set_finger_friendly(item))
1057 if gl.bluetooth_available:
1058 item = gtk.ImageMenuItem(_('Send via bluetooth'))
1059 item.set_image(gtk.image_new_from_icon_name('bluetooth', gtk.ICON_SIZE_MENU))
1060 item.connect('activate', lambda w: self.copy_episode_bluetooth(episode_url))
1061 menu.append(self.set_finger_friendly(item))
1062 if can_transfer:
1063 item = gtk.ImageMenuItem(_('Transfer to %s') % gl.get_device_name())
1064 item.set_image(gtk.image_new_from_icon_name('multimedia-player', gtk.ICON_SIZE_MENU))
1065 item.connect('activate', lambda w: self.on_treeAvailable_row_activated(self.toolTransfer))
1066 menu.append(self.set_finger_friendly(item))
1068 if can_play:
1069 menu.append( gtk.SeparatorMenuItem())
1070 is_played = episode['is_played']
1071 if is_played:
1072 item = gtk.ImageMenuItem(_('Mark as unplayed'))
1073 item.set_image( gtk.image_new_from_stock( gtk.STOCK_CANCEL, gtk.ICON_SIZE_MENU))
1074 item.connect( 'activate', lambda w: self.on_item_toggle_played_activate( w, False, False))
1075 menu.append(self.set_finger_friendly(item))
1076 else:
1077 item = gtk.ImageMenuItem(_('Mark as played'))
1078 item.set_image( gtk.image_new_from_stock( gtk.STOCK_APPLY, gtk.ICON_SIZE_MENU))
1079 item.connect( 'activate', lambda w: self.on_item_toggle_played_activate( w, False, True))
1080 menu.append(self.set_finger_friendly(item))
1082 is_locked = episode['is_locked']
1083 if is_locked:
1084 item = gtk.ImageMenuItem(_('Allow deletion'))
1085 item.set_image(gtk.image_new_from_stock(gtk.STOCK_DIALOG_AUTHENTICATION, gtk.ICON_SIZE_MENU))
1086 item.connect('activate', self.on_item_toggle_lock_activate)
1087 menu.append(self.set_finger_friendly(item))
1088 else:
1089 item = gtk.ImageMenuItem(_('Prohibit deletion'))
1090 item.set_image(gtk.image_new_from_stock(gtk.STOCK_DIALOG_AUTHENTICATION, gtk.ICON_SIZE_MENU))
1091 item.connect('activate', self.on_item_toggle_lock_activate)
1092 menu.append(self.set_finger_friendly(item))
1094 if len(paths) == 1:
1095 menu.append(gtk.SeparatorMenuItem())
1096 # Single item, add episode information menu item
1097 episode_url = model.get_value( model.get_iter( paths[0]), 0)
1098 item = gtk.ImageMenuItem(_('Episode details'))
1099 item.set_image( gtk.image_new_from_stock( gtk.STOCK_INFO, gtk.ICON_SIZE_MENU))
1100 item.connect( 'activate', lambda w: self.on_treeAvailable_row_activated( self.treeAvailable))
1101 menu.append(self.set_finger_friendly(item))
1102 episode = self.active_channel.find_episode(episode_url)
1103 # If we have it, also add episode website link
1104 if episode and episode.link and episode.link != episode.url:
1105 item = gtk.ImageMenuItem(_('Visit website'))
1106 item.set_image(gtk.image_new_from_icon_name(WEB_BROWSER_ICON, gtk.ICON_SIZE_MENU))
1107 item.connect('activate', lambda w: util.open_website(episode.link))
1108 menu.append(self.set_finger_friendly(item))
1110 if gpodder.interface == gpodder.MAEMO:
1111 # Because we open the popup on left-click for Maemo,
1112 # we also include a non-action to close the menu
1113 menu.append(gtk.SeparatorMenuItem())
1114 item = gtk.ImageMenuItem(_('Close this menu'))
1115 item.set_image(gtk.image_new_from_stock(gtk.STOCK_CLOSE, gtk.ICON_SIZE_MENU))
1116 menu.append(self.set_finger_friendly(item))
1118 menu.show_all()
1119 # Disable tooltips while we are showing the menu, so
1120 # the tooltip will not appear over the menu
1121 self.episode_list_can_tooltip = False
1122 menu.connect('deactivate', lambda menushell: self.episode_list_allow_tooltips())
1123 menu.popup( None, None, None, event.button, event.time)
1125 return True
1127 def set_title(self, new_title):
1128 self.default_title = new_title
1129 self.gPodder.set_title(new_title)
1131 def download_progress_updated( self, count, percentage):
1132 title = [ self.default_title ]
1134 total_speed = gl.format_filesize(services.download_status_manager.total_speed())
1136 if count == 1:
1137 title.append( _('downloading one file'))
1138 elif count > 1:
1139 title.append( _('downloading %d files') % count)
1141 if len(title) == 2:
1142 title[1] = ''.join( [ title[1], ' (%d%%, %s/s)' % (percentage, total_speed) ])
1144 self.gPodder.set_title( ' - '.join( title))
1146 # Have all the downloads completed?
1147 # If so execute user command if defined, else do nothing
1148 if count == 0:
1149 if len(gl.config.cmd_all_downloads_complete) > 0:
1150 Thread(target=gl.ext_command_thread, args=(self.notification,gl.config.cmd_all_downloads_complete)).start()
1152 def update_selected_episode_list_icons(self):
1154 Updates the status icons in the episode list
1156 selection = self.treeAvailable.get_selection()
1157 (model, paths) = selection.get_selected_rows()
1158 for path in paths:
1159 iter = model.get_iter(path)
1160 self.active_channel.iter_set_downloading_columns(model, iter)
1162 def update_episode_list_icons(self, urls):
1164 Updates the status icons in the episode list
1165 Only update the episodes that have an URL in
1166 the "urls" iterable object (e.g. a list of URLs)
1168 if self.active_channel is None:
1169 return
1171 model = self.treeAvailable.get_model()
1172 if model is None:
1173 return
1175 for url in urls:
1176 if url in self.url_path_mapping:
1177 path = (self.url_path_mapping[url],)
1178 self.active_channel.iter_set_downloading_columns(model, model.get_iter(path))
1180 def playback_episode(self, episode, stream=False):
1181 (success, application) = gl.playback_episode(episode, stream)
1182 if not success:
1183 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), ))
1184 self.update_selected_episode_list_icons()
1185 self.updateComboBox(only_selected_channel=True)
1187 def treeAvailable_search_equal( self, model, column, key, iter, data = None):
1188 if model is None:
1189 return True
1191 key = key.lower()
1193 # columns, as defined in libpodcasts' get model method
1194 # 1 = episode title, 7 = description
1195 columns = (1, 7)
1197 for column in columns:
1198 value = model.get_value( iter, column).lower()
1199 if value.find( key) != -1:
1200 return False
1202 return True
1204 def change_menu_item(self, menuitem, icon=None, label=None):
1205 if icon is not None:
1206 menuitem.get_image().set_from_icon_name(icon, gtk.ICON_SIZE_MENU)
1207 if label is not None:
1208 label_widget = menuitem.get_child()
1209 label_widget.set_text(label)
1211 def play_or_download(self):
1212 if self.wNotebook.get_current_page() > 0:
1213 return
1215 ( can_play, can_download, can_transfer, can_cancel, can_delete ) = (False,)*5
1216 ( is_played, is_locked ) = (False,)*2
1218 open_instead_of_play = False
1220 selection = self.treeAvailable.get_selection()
1221 if selection.count_selected_rows() > 0:
1222 (model, paths) = selection.get_selected_rows()
1224 for path in paths:
1225 url = model.get_value( model.get_iter( path), 0)
1226 local_filename = model.get_value( model.get_iter( path), 8)
1228 episode = podcastItem.load(url, self.active_channel)
1230 if episode.file_type() not in ('audio', 'video'):
1231 open_instead_of_play = True
1233 if episode.was_downloaded():
1234 can_play = episode.was_downloaded(and_exists=True)
1235 can_delete = True
1236 is_played = episode.is_played
1237 is_locked = episode.is_locked
1238 if not can_play:
1239 can_download = True
1240 else:
1241 if services.download_status_manager.is_download_in_progress(url):
1242 can_cancel = True
1243 else:
1244 can_download = True
1246 can_download = can_download and not can_cancel
1247 can_play = gl.config.enable_streaming or (can_play and not can_cancel and not can_download)
1248 can_transfer = can_play and gl.config.device_type != 'none' and not can_cancel and not can_download
1250 if open_instead_of_play:
1251 self.toolPlay.set_stock_id(gtk.STOCK_OPEN)
1252 can_transfer = False
1253 else:
1254 self.toolPlay.set_stock_id(gtk.STOCK_MEDIA_PLAY)
1256 self.toolPlay.set_sensitive( can_play)
1257 self.toolDownload.set_sensitive( can_download)
1258 self.toolTransfer.set_sensitive( can_transfer)
1259 self.toolCancel.set_sensitive( can_cancel)
1261 if can_cancel:
1262 self.item_cancel_download.show_all()
1263 else:
1264 self.item_cancel_download.hide_all()
1265 if can_download:
1266 self.itemDownloadSelected.show_all()
1267 else:
1268 self.itemDownloadSelected.hide_all()
1269 if can_play:
1270 if open_instead_of_play:
1271 self.itemOpenSelected.show_all()
1272 self.itemPlaySelected.hide_all()
1273 else:
1274 self.itemPlaySelected.show_all()
1275 self.itemOpenSelected.hide_all()
1276 if not can_download:
1277 self.itemDeleteSelected.show_all()
1278 else:
1279 self.itemDeleteSelected.hide_all()
1280 self.item_toggle_played.show_all()
1281 self.item_toggle_lock.show_all()
1282 self.separator9.show_all()
1283 if is_played:
1284 self.change_menu_item(self.item_toggle_played, gtk.STOCK_CANCEL, _('Mark as unplayed'))
1285 else:
1286 self.change_menu_item(self.item_toggle_played, gtk.STOCK_APPLY, _('Mark as played'))
1287 if is_locked:
1288 self.change_menu_item(self.item_toggle_lock, gtk.STOCK_DIALOG_AUTHENTICATION, _('Allow deletion'))
1289 else:
1290 self.change_menu_item(self.item_toggle_lock, gtk.STOCK_DIALOG_AUTHENTICATION, _('Prohibit deletion'))
1291 else:
1292 self.itemPlaySelected.hide_all()
1293 self.itemOpenSelected.hide_all()
1294 self.itemDeleteSelected.hide_all()
1295 self.item_toggle_played.hide_all()
1296 self.item_toggle_lock.hide_all()
1297 self.separator9.hide_all()
1298 if can_play or can_download or can_cancel:
1299 self.item_episode_details.show_all()
1300 self.separator16.show_all()
1301 self.no_episode_selected.hide_all()
1302 else:
1303 self.item_episode_details.hide_all()
1304 self.separator16.hide_all()
1305 self.no_episode_selected.show_all()
1307 return (can_play, can_download, can_transfer, can_cancel, can_delete, open_instead_of_play)
1309 def download_status_updated(self, episode_urls, channel_urls):
1310 count = services.download_status_manager.count()
1311 if count:
1312 self.labelDownloads.set_text( _('Downloads (%d)') % count)
1313 else:
1314 self.labelDownloads.set_text( _('Downloads'))
1316 self.update_episode_list_icons(episode_urls)
1317 self.updateComboBox(only_these_urls=channel_urls)
1319 def on_cbMaxDownloads_toggled(self, widget, *args):
1320 self.spinMaxDownloads.set_sensitive(self.cbMaxDownloads.get_active())
1322 def on_cbLimitDownloads_toggled(self, widget, *args):
1323 self.spinLimitDownloads.set_sensitive(self.cbLimitDownloads.get_active())
1325 def episode_new_status_changed(self, urls):
1326 self.updateComboBox()
1327 self.update_episode_list_icons(urls)
1329 def updateComboBox(self, selected_url=None, only_selected_channel=False, only_these_urls=None):
1330 selection = self.treeChannels.get_selection()
1331 (model, iter) = selection.get_selected()
1333 if only_selected_channel:
1334 # very cheap! only update selected channel
1335 if iter and self.active_channel is not None:
1336 update_channel_model_by_iter(model, iter,
1337 self.active_channel, self.channel_colors,
1338 self.cover_cache,
1339 gl.config.podcast_list_icon_size,
1340 gl.config.podcast_list_icon_size)
1341 elif not self.channel_list_changed:
1342 # we can keep the model, but have to update some
1343 if only_these_urls is None:
1344 # still cheaper than reloading the whole list
1345 iter = model.get_iter_first()
1346 while iter is not None:
1347 (index,) = model.get_path(iter)
1348 update_channel_model_by_iter(model, iter,
1349 self.channels[index], self.channel_colors,
1350 self.cover_cache,
1351 gl.config.podcast_list_icon_size,
1352 gl.config.podcast_list_icon_size)
1353 iter = model.iter_next(iter)
1354 else:
1355 # ok, we got a bunch of urls to update
1356 for url in only_these_urls:
1357 if url in self.channel_url_path_mapping:
1358 index = self.channel_url_path_mapping[url]
1359 path = (index,)
1360 iter = model.get_iter(path)
1361 update_channel_model_by_iter(model, iter,
1362 self.channels[index], self.channel_colors,
1363 self.cover_cache,
1364 gl.config.podcast_list_icon_size,
1365 gl.config.podcast_list_icon_size)
1366 else:
1367 if model and iter and selected_url is None:
1368 # Get the URL of the currently-selected podcast
1369 selected_url = model.get_value(iter, 0)
1371 (model, urls) = channels_to_model(self.channels,
1372 self.channel_colors, self.cover_cache,
1373 gl.config.podcast_list_icon_size,
1374 gl.config.podcast_list_icon_size)
1376 self.channel_url_path_mapping = dict(zip(urls, range(len(urls))))
1377 self.treeChannels.set_model(model)
1379 try:
1380 selected_path = (0,)
1381 # Find the previously-selected URL in the new
1382 # model if we have an URL (else select first)
1383 if selected_url is not None:
1384 pos = model.get_iter_first()
1385 while pos is not None:
1386 url = model.get_value(pos, 0)
1387 if url == selected_url:
1388 selected_path = model.get_path(pos)
1389 break
1390 pos = model.iter_next(pos)
1392 self.treeChannels.get_selection().select_path(selected_path)
1393 except:
1394 log( 'Cannot set selection on treeChannels', sender = self)
1395 self.on_treeChannels_cursor_changed( self.treeChannels)
1396 self.channel_list_changed = False
1398 def updateTreeView(self):
1399 if self.channels and self.active_channel is not None:
1400 (model, urls) = self.active_channel.get_tree_model()
1401 self.treeAvailable.set_model(model)
1402 self.url_path_mapping = dict(zip(urls, range(len(urls))))
1403 self.treeAvailable.columns_autosize()
1404 self.play_or_download()
1405 else:
1406 model = self.treeAvailable.get_model()
1407 if model is not None:
1408 model.clear()
1410 def drag_data_received(self, widget, context, x, y, sel, ttype, time):
1411 (path, column, rx, ry) = self.treeChannels.get_path_at_pos( x, y) or (None,)*4
1413 dnd_channel = None
1414 if path is not None:
1415 model = self.treeChannels.get_model()
1416 iter = model.get_iter(path)
1417 url = model.get_value(iter, 0)
1418 for channel in self.channels:
1419 if channel.url == url:
1420 dnd_channel = channel
1421 break
1423 result = sel.data
1424 rl = result.strip().lower()
1425 if (rl.endswith('.jpg') or rl.endswith('.png') or rl.endswith('.gif') or rl.endswith('.svg')) and dnd_channel is not None:
1426 services.cover_downloader.replace_cover(dnd_channel, result)
1427 else:
1428 self.add_new_channel(result)
1430 def add_new_channel(self, result=None, ask_download_new=True, quiet=False, block=False, authentication_tokens=None):
1431 (scheme, rest) = result.split('://', 1)
1432 result = util.normalize_feed_url(result)
1434 if not result:
1435 cute_scheme = saxutils.escape(scheme)+'://'
1436 title = _('%s URLs are not supported') % cute_scheme
1437 message = _('gPodder does not understand the URL you supplied.')
1438 self.show_message( message, title)
1439 return
1441 for old_channel in self.channels:
1442 if old_channel.url == result:
1443 log( 'Channel already exists: %s', result)
1444 # Select the existing channel in combo box
1445 for i in range( len( self.channels)):
1446 if self.channels[i] == old_channel:
1447 self.treeChannels.get_selection().select_path( (i,))
1448 self.on_treeChannels_cursor_changed(self.treeChannels)
1449 break
1450 self.show_message( _('You have already subscribed to this podcast: %s') % (
1451 saxutils.escape( old_channel.title), ), _('Already added'))
1452 return
1454 waitdlg = gtk.MessageDialog(self.gPodder, 0, gtk.MESSAGE_INFO, gtk.BUTTONS_NONE)
1455 waitdlg.add_button(gtk.STOCK_CANCEL, gtk.RESPONSE_CANCEL)
1456 waitdlg.set_title(_('Downloading episode list'))
1457 waitdlg.set_markup('<b><big>%s</big></b>' % waitdlg.get_title())
1458 waitdlg.format_secondary_text(_('Please wait while I am downloading episode information for %s') % result)
1459 waitpb = gtk.ProgressBar()
1460 if block:
1461 waitdlg.vbox.add(waitpb)
1462 waitdlg.show_all()
1463 waitdlg.set_response_sensitive(gtk.RESPONSE_CANCEL, False)
1465 self.entryAddChannel.set_text(_('Downloading feed...'))
1466 self.entryAddChannel.set_sensitive(False)
1467 self.btnAddChannel.set_sensitive(False)
1468 args = (result, self.add_new_channel_finish, authentication_tokens, ask_download_new, quiet, waitdlg)
1469 thread = Thread( target=self.add_new_channel_proc, args=args )
1470 thread.start()
1472 while block and thread.isAlive():
1473 while gtk.events_pending():
1474 gtk.main_iteration( False)
1475 waitpb.pulse()
1476 time.sleep(0.05)
1479 def add_new_channel_proc( self, url, callback, authentication_tokens, *callback_args):
1480 log( 'Adding new channel: %s', url)
1481 channel = error = None
1482 try:
1483 channel = podcastChannel.load(url=url, create=True, authentication_tokens=authentication_tokens)
1484 except HTTPAuthError, e:
1485 error = e
1486 except Exception, e:
1487 log('Error in podcastChannel.load(%s): %s', url, e, traceback=True, sender=self)
1489 util.idle_add( callback, channel, url, error, *callback_args )
1491 def add_new_channel_finish( self, channel, url, error, ask_download_new, quiet, waitdlg):
1492 if channel is not None:
1493 self.channels.append( channel)
1494 self.channel_list_changed = True
1495 save_channels( self.channels)
1496 if not quiet:
1497 # download changed channels and select the new episode in the UI afterwards
1498 self.update_feed_cache(force_update=False, select_url_afterwards=channel.url)
1500 (username, password) = util.username_password_from_url( url)
1501 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')):
1502 channel.username = username
1503 channel.password = password
1504 log('Saving authentication data for episode downloads..', sender = self)
1505 channel.save()
1506 # We need to update the channel list otherwise the authentication
1507 # data won't show up in the channel editor.
1508 # TODO: Only updated the newly added feed to save some cpu cycles
1509 self.channels = load_channels()
1510 self.channel_list_changed = True
1512 if ask_download_new:
1513 new_episodes = channel.get_new_episodes()
1514 if len(new_episodes):
1515 self.new_episodes_show(new_episodes)
1517 elif isinstance( error, HTTPAuthError ):
1518 response, auth_tokens = self.UsernamePasswordDialog(
1519 _('Feed requires authentication'), _('Please enter your username and password.'))
1521 if response:
1522 self.add_new_channel( url, authentication_tokens=auth_tokens )
1524 else:
1525 # Ok, the URL is not a channel, or there is some other
1526 # error - let's see if it's a web page or OPML file...
1527 try:
1528 data = urllib2.urlopen(url).read().lower()
1529 if '</opml>' in data:
1530 # This looks like an OPML feed
1531 self.on_item_import_from_file_activate(None, url)
1533 elif '</html>' in data:
1534 # This looks like a web page
1535 title = _('The URL is a website')
1536 message = _('The URL you specified points to a web page. You need to find the "feed" URL of the podcast to add to gPodder. Do you want to visit this website now and look for the podcast feed URL?\n\n(Hint: Look for "XML feed", "RSS feed" or "Podcast feed" if you are unsure for what to look. If there is only an iTunes URL, try adding this one.)')
1537 if self.show_confirmation(message, title):
1538 util.open_website(url)
1540 except Exception, e:
1541 log('Error trying to handle the URL as OPML or web page: %s', e, sender=self)
1543 title = _('Error adding podcast')
1544 message = _('The podcast could not be added. Please check the spelling of the URL or try again later.')
1545 self.show_message( message, title)
1547 self.entryAddChannel.set_text(self.ENTER_URL_TEXT)
1548 self.entryAddChannel.set_sensitive(True)
1549 self.btnAddChannel.set_sensitive(True)
1550 self.update_podcasts_tab()
1551 waitdlg.destroy()
1554 def update_feed_cache_finish_callback(self, channels=None,
1555 notify_no_new_episodes=False, select_url_afterwards=None):
1557 db.commit()
1559 self.updating_feed_cache = False
1560 self.hboxUpdateFeeds.hide_all()
1561 self.btnUpdateFeeds.show_all()
1562 self.itemUpdate.set_sensitive(True)
1563 self.itemUpdateChannel.set_sensitive(True)
1565 # If we want to select a specific podcast (via its URL)
1566 # after the update, we give it to updateComboBox here to
1567 # select exactly this podcast after updating the view
1568 self.updateComboBox(selected_url=select_url_afterwards)
1570 if self.tray_icon:
1571 self.tray_icon.set_status(None)
1572 if self.minimized:
1573 new_episodes = []
1574 # look for new episodes to notify
1575 for channel in self.channels:
1576 for episode in channel.get_new_episodes():
1577 if not episode in self.already_notified_new_episodes:
1578 new_episodes.append(episode)
1579 self.already_notified_new_episodes.append(episode)
1580 # notify new episodes
1582 if len(new_episodes) == 0:
1583 if notify_no_new_episodes and self.tray_icon is not None:
1584 msg = _('No new episodes available for download')
1585 self.tray_icon.send_notification(msg)
1586 return
1587 elif len(new_episodes) == 1:
1588 title = _('gPodder has found %s') % (_('one new episode:'),)
1589 else:
1590 title = _('gPodder has found %s') % (_('%i new episodes:') % len(new_episodes))
1591 message = self.tray_icon.format_episode_list(new_episodes)
1593 #auto download new episodes
1594 if gl.config.auto_download_when_minimized:
1595 message += '\n<i>(%s...)</i>' % _('downloading')
1596 self.download_episode_list(new_episodes)
1597 self.tray_icon.send_notification(message, title)
1598 return
1600 # open the episodes selection dialog
1601 self.channels = load_channels()
1602 self.channel_list_changed = True
1603 self.updateComboBox()
1604 if not self.feed_cache_update_cancelled:
1605 self.download_all_new(channels=channels)
1607 def update_feed_cache_callback(self, progressbar, title, position, count):
1608 progression = _('Updated %s (%d/%d)')%(title, position+1, count)
1609 progressbar.set_text(progression)
1610 if self.tray_icon:
1611 self.tray_icon.set_status(
1612 self.tray_icon.STATUS_UPDATING_FEED_CACHE, progression )
1613 if count > 0:
1614 progressbar.set_fraction(float(position)/float(count))
1616 def update_feed_cache_proc( self, channel, total_channels, semaphore,
1617 callback_proc, finish_proc):
1619 semaphore.acquire()
1620 if not self.feed_cache_update_cancelled:
1621 try:
1622 channel.update()
1623 except:
1624 log('Darn SQLite LOCK!', sender=self, traceback=True)
1626 # By the time we get here the update may have already been cancelled
1627 if not self.feed_cache_update_cancelled:
1628 callback_proc(channel.title, self.updated_feeds, total_channels)
1630 self.updated_feeds += 1
1631 self.treeview_channel_set_color( channel, 'default' )
1632 channel.update_flag = False
1634 semaphore.release()
1635 if self.updated_feeds == total_channels:
1636 finish_proc()
1638 def on_btnCancelFeedUpdate_clicked(self, widget):
1639 self.pbFeedUpdate.set_text(_('Cancelling...'))
1640 self.feed_cache_update_cancelled = True
1642 def update_feed_cache(self, channels=None, force_update=True,
1643 notify_no_new_episodes=False, select_url_afterwards=None):
1645 if self.updating_feed_cache:
1646 return
1648 if not force_update:
1649 self.channels = load_channels()
1650 self.channel_list_changed = True
1651 self.updateComboBox()
1652 return
1654 self.updating_feed_cache = True
1655 self.itemUpdate.set_sensitive(False)
1656 self.itemUpdateChannel.set_sensitive(False)
1658 if self.tray_icon:
1659 self.tray_icon.set_status(self.tray_icon.STATUS_UPDATING_FEED_CACHE)
1661 if channels is None:
1662 channels = self.channels
1664 if len(channels) == 1:
1665 text = _('Updating %d feed.')
1666 else:
1667 text = _('Updating %d feeds.')
1668 self.pbFeedUpdate.set_text( text % len(channels))
1669 self.pbFeedUpdate.set_fraction(0)
1671 # let's get down to business..
1672 callback_proc = lambda title, pos, count: util.idle_add(
1673 self.update_feed_cache_callback, self.pbFeedUpdate, title, pos, count )
1674 finish_proc = lambda: util.idle_add( self.update_feed_cache_finish_callback,
1675 channels, notify_no_new_episodes, select_url_afterwards )
1677 self.updated_feeds = 0
1678 self.feed_cache_update_cancelled = False
1679 self.btnUpdateFeeds.hide_all()
1680 self.hboxUpdateFeeds.show_all()
1681 semaphore = Semaphore(gl.config.max_simulaneous_feeds_updating)
1683 for channel in channels:
1684 self.treeview_channel_set_color( channel, 'updating' )
1685 channel.update_flag = True
1686 args = (channel, len(channels), semaphore, callback_proc, finish_proc)
1687 thread = Thread( target = self.update_feed_cache_proc, args = args)
1688 thread.start()
1690 def treeview_channel_set_color( self, channel, color ):
1691 if self.treeChannels.get_model():
1692 if color in self.channel_colors:
1693 self.treeChannels.get_model().set(channel.iter, 8, self.channel_colors[color])
1694 else:
1695 self.treeChannels.get_model().set(channel.iter, 8, color)
1697 def on_gPodder_delete_event(self, widget, *args):
1698 """Called when the GUI wants to close the window
1699 Displays a confirmation dialog (and closes/hides gPodder)
1702 downloading = services.download_status_manager.has_items()
1704 # Only iconify if we are using the window's "X" button,
1705 # but not when we are using "Quit" in the menu or toolbar
1706 if not gl.config.on_quit_ask and gl.config.on_quit_systray and self.tray_icon and widget.name not in ('toolQuit', 'itemQuit'):
1707 self.iconify_main_window()
1708 elif gl.config.on_quit_ask or downloading:
1709 if gpodder.interface == gpodder.MAEMO:
1710 result = self.show_confirmation(_('Do you really want to quit gPodder now?'))
1711 if result:
1712 self.close_gpodder()
1713 else:
1714 return True
1715 dialog = gtk.MessageDialog(self.gPodder, gtk.DIALOG_MODAL, gtk.MESSAGE_QUESTION, gtk.BUTTONS_NONE)
1716 dialog.add_button(gtk.STOCK_CANCEL, gtk.RESPONSE_CANCEL)
1717 dialog.add_button(gtk.STOCK_QUIT, gtk.RESPONSE_CLOSE)
1719 title = _('Quit gPodder')
1720 if downloading:
1721 message = _('You are downloading episodes. If you close gPodder now, the downloads will be aborted.')
1722 else:
1723 message = _('Do you really want to quit gPodder now?')
1725 dialog.set_title(title)
1726 dialog.set_markup('<span weight="bold" size="larger">%s</span>\n\n%s'%(title, message))
1727 if not downloading:
1728 cb_ask = gtk.CheckButton(_("Don't ask me again"))
1729 dialog.vbox.pack_start(cb_ask)
1730 cb_ask.show_all()
1732 result = dialog.run()
1733 dialog.destroy()
1735 if result == gtk.RESPONSE_CLOSE:
1736 if not downloading and cb_ask.get_active() == True:
1737 gl.config.on_quit_ask = False
1738 self.close_gpodder()
1739 else:
1740 self.close_gpodder()
1742 return True
1744 def close_gpodder(self):
1745 """ clean everything and exit properly
1747 if self.channels:
1748 if save_channels(self.channels):
1749 if gl.config.my_gpodder_autoupload:
1750 log('Uploading to my.gpodder.org on close', sender=self)
1751 util.idle_add(self.on_upload_to_mygpo, None)
1752 else:
1753 self.show_message(_('Please check your permissions and free disk space.'), _('Error saving podcast list'))
1755 services.download_status_manager.cancel_all()
1756 self.gPodder.hide()
1757 while gtk.events_pending():
1758 gtk.main_iteration(False)
1760 db.close()
1762 self.gtk_main_quit()
1763 sys.exit( 0)
1765 def get_old_episodes(self):
1766 episodes = []
1767 for channel in self.channels:
1768 for episode in channel.get_downloaded_episodes():
1769 if episode.is_old() and not episode.is_locked and episode.is_played:
1770 episodes.append(episode)
1771 return episodes
1773 def for_each_selected_episode_url( self, callback):
1774 ( model, paths ) = self.treeAvailable.get_selection().get_selected_rows()
1775 for path in paths:
1776 url = model.get_value( model.get_iter( path), 0)
1777 try:
1778 callback( url)
1779 except Exception, e:
1780 log( 'Warning: Error in for_each_selected_episode_url for URL %s: %s', url, e, sender = self)
1782 self.update_selected_episode_list_icons()
1783 self.updateComboBox(only_selected_channel=True)
1785 def delete_episode_list( self, episodes, confirm = True):
1786 if len(episodes) == 0:
1787 return
1789 if len(episodes) == 1:
1790 message = _('Do you really want to delete this episode?')
1791 else:
1792 message = _('Do you really want to delete %d episodes?') % len(episodes)
1794 if confirm and self.show_confirmation( message, _('Delete episodes')) == False:
1795 return
1797 episode_urls = set()
1798 channel_urls = set()
1799 for episode in episodes:
1800 log('Deleting episode: %s', episode.title, sender = self)
1801 episode.delete_from_disk()
1802 episode_urls.add(episode.url)
1803 channel_urls.add(episode.channel.url)
1805 self.download_status_updated(episode_urls, channel_urls)
1807 def on_itemRemoveOldEpisodes_activate( self, widget):
1808 columns = (
1809 ('title_and_description', None, None, _('Episode')),
1810 ('channel_prop', None, None, _('Podcast')),
1811 ('filesize_prop', 'length', gobject.TYPE_INT, _('Size')),
1812 ('pubdate_prop', 'pubDate', gobject.TYPE_INT, _('Released')),
1813 ('played_prop', None, None, _('Status')),
1814 ('age_prop', None, None, _('Downloaded')),
1817 selection_buttons = {
1818 _('Select played'): lambda episode: episode.is_played,
1819 _('Select older than %d days') % gl.config.episode_old_age: lambda episode: episode.is_old(),
1822 instructions = _('Select the episodes you want to delete from your hard disk.')
1824 episodes = []
1825 selected = []
1826 for channel in self.channels:
1827 for episode in channel.get_downloaded_episodes():
1828 if not episode.is_locked:
1829 episodes.append(episode)
1830 selected.append(episode.is_played)
1832 gPodderEpisodeSelector( title = _('Remove old episodes'), instructions = instructions, \
1833 episodes = episodes, selected = selected, columns = columns, \
1834 stock_ok_button = gtk.STOCK_DELETE, callback = self.delete_episode_list, \
1835 selection_buttons = selection_buttons)
1837 def mark_selected_episodes_new(self):
1838 callback = lambda url: self.active_channel.find_episode(url).mark_new()
1839 self.for_each_selected_episode_url(callback)
1841 def mark_selected_episodes_old(self):
1842 callback = lambda url: self.active_channel.find_episode(url).mark_old()
1843 self.for_each_selected_episode_url(callback)
1845 def on_item_toggle_played_activate( self, widget, toggle = True, new_value = False):
1846 if toggle:
1847 callback = lambda url: db.mark_episode(url, is_played=True, toggle=True)
1848 else:
1849 callback = lambda url: db.mark_episode(url, is_played=new_value)
1851 self.for_each_selected_episode_url(callback)
1853 def on_item_toggle_lock_activate(self, widget, toggle=True, new_value=False):
1854 if toggle:
1855 callback = lambda url: db.mark_episode(url, is_locked=True, toggle=True)
1856 else:
1857 callback = lambda url: db.mark_episode(url, is_locked=new_value)
1859 self.for_each_selected_episode_url(callback)
1861 def on_channel_toggle_lock_activate(self, widget, toggle=True, new_value=False):
1862 self.active_channel.channel_is_locked = not self.active_channel.channel_is_locked
1863 db.update_channel_lock(self.active_channel)
1865 if self.active_channel.channel_is_locked:
1866 self.change_menu_item(self.channel_toggle_lock, gtk.STOCK_DIALOG_AUTHENTICATION, _('Allow deletion of all episodes'))
1867 else:
1868 self.change_menu_item(self.channel_toggle_lock, gtk.STOCK_DIALOG_AUTHENTICATION, _('Prohibit deletion of all episodes'))
1870 for episode in self.active_channel.get_all_episodes():
1871 db.mark_episode(episode.url, is_locked=self.active_channel.channel_is_locked)
1873 self.updateComboBox(only_selected_channel=True)
1875 def on_item_email_subscriptions_activate(self, widget):
1876 if not self.channels:
1877 self.show_message(_('Your subscription list is empty.'), _('Could not send list'))
1878 elif not gl.send_subscriptions():
1879 self.show_message(_('There was an error sending your subscription list via e-mail.'), _('Could not send list'))
1881 def on_itemUpdateChannel_activate(self, widget=None):
1882 self.update_feed_cache(channels=[self.active_channel,])
1884 def on_itemUpdate_activate(self, widget, notify_no_new_episodes=False):
1885 restore_from = can_restore_from_opml()
1887 if self.channels:
1888 self.update_feed_cache(notify_no_new_episodes=notify_no_new_episodes)
1889 elif restore_from is not None:
1890 title = _('Database upgrade required')
1891 message = _('gPodder is now using a new (much faster) database backend and needs to convert your current data. This can take some time. Start the conversion now?')
1892 if self.show_confirmation(message, title):
1893 add_callback = lambda url: self.add_new_channel(url, False, True)
1894 w = gtk.Dialog(_('Migrating to SQLite'), self.gPodder, 0, (gtk.STOCK_CLOSE, gtk.RESPONSE_ACCEPT))
1895 w.set_has_separator(False)
1896 w.set_response_sensitive(gtk.RESPONSE_ACCEPT, False)
1897 w.set_default_size(500, -1)
1898 pb = gtk.ProgressBar()
1899 l = gtk.Label()
1900 l.set_padding(6, 3)
1901 l.set_markup('<b><big>%s</big></b>' % _('SQLite migration'))
1902 l.set_alignment(0.0, 0.5)
1903 w.vbox.pack_start(l)
1904 l = gtk.Label()
1905 l.set_padding(6, 3)
1906 l.set_alignment(0.0, 0.5)
1907 l.set_text(_('Please wait while your settings are converted.'))
1908 w.vbox.pack_start(l)
1909 w.vbox.pack_start(pb)
1910 lb = gtk.Label()
1911 lb.set_ellipsize(pango.ELLIPSIZE_END)
1912 lb.set_alignment(0.0, 0.5)
1913 lb.set_padding(6, 6)
1914 w.vbox.pack_start(lb)
1916 def set_pb_status(pb, lb, fraction, text):
1917 pb.set_fraction(float(fraction)/100.0)
1918 pb.set_text('%.0f %%' % fraction)
1919 lb.set_markup('<i>%s</i>' % saxutils.escape(text))
1920 while gtk.events_pending():
1921 gtk.main_iteration(False)
1922 status_callback = lambda fraction, text: set_pb_status(pb, lb, fraction, text)
1923 get_localdb = lambda channel: LocalDBReader(channel.url).read(channel.index_file)
1924 w.show_all()
1925 start = datetime.datetime.now()
1926 gl.migrate_to_sqlite(add_callback, status_callback, load_channels, get_localdb)
1927 # Refresh the view with the updated episodes
1928 self.updateComboBox()
1929 time_taken = str(datetime.datetime.now()-start)
1930 status_callback(100.0, _('Migration finished in %s') % time_taken)
1931 w.set_response_sensitive(gtk.RESPONSE_ACCEPT, True)
1932 w.run()
1933 w.destroy()
1934 else:
1935 gPodderWelcome(center_on_widget=self.gPodder, show_example_podcasts_callback=self.on_itemImportChannels_activate, setup_my_gpodder_callback=self.on_download_from_mygpo)
1937 def download_episode_list(self, episodes):
1938 services.download_status_manager.start_batch_mode()
1939 for episode in episodes:
1940 log('Downloading episode: %s', episode.title, sender = self)
1941 filename = episode.local_filename()
1942 if not episode.was_downloaded(and_exists=True) and not services.download_status_manager.is_download_in_progress( episode.url):
1943 download.DownloadThread(episode.channel, episode, self.notification).start()
1944 services.download_status_manager.end_batch_mode()
1946 def new_episodes_show(self, episodes):
1947 columns = (
1948 ('title_and_description', None, None, _('Episode')),
1949 ('channel_prop', None, None, _('Podcast')),
1950 ('filesize_prop', 'length', gobject.TYPE_INT, _('Size')),
1951 ('pubdate_prop', 'pubDate', gobject.TYPE_INT, _('Released')),
1954 if len(episodes) > 0:
1955 instructions = _('Select the episodes you want to download now.')
1957 gPodderEpisodeSelector(title=_('New episodes available'), instructions=instructions, \
1958 episodes=episodes, columns=columns, selected_default=True, \
1959 stock_ok_button = 'gpodder-download', \
1960 callback=self.download_episode_list, \
1961 remove_callback=lambda e: e.mark_old(), \
1962 remove_action=_('Never download'), \
1963 remove_finished=self.episode_new_status_changed)
1964 else:
1965 title = _('No new episodes')
1966 message = _('No new episodes to download.\nPlease check for new episodes later.')
1967 self.show_message(message, title)
1969 def on_itemDownloadAllNew_activate(self, widget, *args):
1970 self.download_all_new()
1972 def download_all_new(self, channels=None):
1973 if channels is None:
1974 channels = self.channels
1975 episodes = []
1976 for channel in channels:
1977 for episode in channel.get_new_episodes():
1978 episodes.append(episode)
1979 self.new_episodes_show(episodes)
1981 def get_all_episodes(self, exclude_nonsignificant=True ):
1982 """'exclude_nonsignificant' will exclude non-downloaded episodes
1983 and all episodes from channels that are set to skip when syncing"""
1984 episode_list = []
1985 for channel in self.channels:
1986 if not channel.sync_to_devices and exclude_nonsignificant:
1987 log('Skipping channel: %s', channel.title, sender=self)
1988 continue
1989 for episode in channel.get_all_episodes():
1990 if episode.was_downloaded(and_exists=True) or not exclude_nonsignificant:
1991 episode_list.append(episode)
1992 return episode_list
1994 def ipod_delete_played(self, device):
1995 all_episodes = self.get_all_episodes( exclude_nonsignificant=False )
1996 episodes_on_device = device.get_all_tracks()
1997 for local_episode in all_episodes:
1998 device_episode = device.episode_on_device(local_episode)
1999 if device_episode and ( local_episode.is_played and not local_episode.is_locked
2000 or local_episode.state == db.STATE_DELETED ):
2001 log("mp3_player_delete_played: removing %s" % device_episode.title)
2002 device.remove_track(device_episode)
2004 def on_sync_to_ipod_activate(self, widget, episodes=None):
2005 # make sure gpod is available before even trying to sync
2006 if gl.config.device_type == 'ipod' and not sync.gpod_available:
2007 title = _('Cannot Sync To iPod')
2008 message = _('Please install the libgpod python bindings (python-gpod) and restart gPodder to continue.')
2009 self.notification( message, title )
2010 return
2011 elif gl.config.device_type == 'mtp' and not sync.pymtp_available:
2012 title = _('Cannot sync to MTP device')
2013 message = _('Please install the libmtp python bindings (python-pymtp) and restart gPodder to continue.')
2014 self.notification( message, title )
2015 return
2017 device = sync.open_device()
2018 device.register( 'post-done', self.sync_to_ipod_completed )
2020 if device is None:
2021 title = _('No device configured')
2022 message = _('To use the synchronization feature, please configure your device in the preferences dialog first.')
2023 self.notification(message, title)
2024 return
2026 if not device.open():
2027 title = _('Cannot open device')
2028 message = _('There has been an error opening your device.')
2029 self.notification(message, title)
2030 return
2032 if gl.config.ipod_purge_old_episodes:
2033 device.purge()
2035 sync_all_episodes = not bool(episodes)
2037 if episodes is None:
2038 episodes = self.get_all_episodes()
2040 # make sure we have enough space on the device
2041 total_size = 0
2042 free_space = device.get_free_space()
2043 for episode in episodes:
2044 if not device.episode_on_device(episode) and not (sync_all_episodes and gl.config.only_sync_not_played and episode.is_played):
2045 total_size += util.calculate_size(str(episode.local_filename()))
2047 if total_size > free_space:
2048 # can be negative because of the 10 MiB for reserved for the iTunesDB
2049 free_space = max( free_space, 0 )
2050 log('(gpodder.sync) Not enough free space. Transfer size = %d, Free space = %d', total_size, free_space)
2051 title = _('Not enough space left on device.')
2052 message = _('%s remaining on device.\nPlease free up %s and try again.' % (
2053 util.format_filesize( free_space ), util.format_filesize( total_size - free_space )))
2054 self.notification(message, title)
2055 else:
2056 # start syncing!
2057 gPodderSync(device=device, gPodder=self)
2058 Thread(target=self.sync_to_ipod_thread, args=(widget, device, sync_all_episodes, episodes)).start()
2059 if self.tray_icon:
2060 self.tray_icon.set_synchronisation_device(device)
2062 def sync_to_ipod_completed(self, device, successful_sync):
2063 device.unregister( 'post-done', self.sync_to_ipod_completed )
2065 if self.tray_icon:
2066 self.tray_icon.release_synchronisation_device()
2068 if not successful_sync:
2069 title = _('Error closing device')
2070 message = _('There has been an error closing your device.')
2071 self.notification(message, title)
2073 # update model for played state updates after sync
2074 util.idle_add(self.updateComboBox)
2076 def sync_to_ipod_thread(self, widget, device, sync_all_episodes, episodes=None):
2077 if sync_all_episodes:
2078 device.add_tracks(episodes)
2079 # 'only_sync_not_played' must be used or else all the played
2080 # tracks will be copied then immediately deleted
2081 if gl.config.mp3_player_delete_played and gl.config.only_sync_not_played:
2082 self.ipod_delete_played(device)
2083 else:
2084 device.add_tracks(episodes, force_played=True)
2085 device.close()
2087 def ipod_cleanup_callback(self, device, tracks):
2088 title = _('Delete podcasts from device?')
2089 message = _('The selected episodes will be removed from your device. This cannot be undone. Files in your gPodder library will be unaffected. Do you really want to delete these episodes from your device?')
2090 if len(tracks) > 0 and self.show_confirmation(message, title):
2091 device.remove_tracks(tracks)
2093 if not device.close():
2094 title = _('Error closing device')
2095 message = _('There has been an error closing your device.')
2096 self.show_message(message, title)
2097 return
2099 def on_cleanup_ipod_activate(self, widget, *args):
2100 columns = (
2101 ('title', None, None, _('Episode')),
2102 ('podcast', None, None, _('Podcast')),
2103 ('filesize', None, None, _('Size')),
2104 ('modified', None, None, _('Copied')),
2105 ('playcount', None, None, _('Play count')),
2106 ('released', None, None, _('Released')),
2109 device = sync.open_device()
2111 if device is None:
2112 title = _('No device configured')
2113 message = _('To use the synchronization feature, please configure your device in the preferences dialog first.')
2114 self.show_message(message, title)
2115 return
2117 if not device.open():
2118 title = _('Cannot open device')
2119 message = _('There has been an error opening your device.')
2120 self.show_message(message, title)
2121 return
2123 gPodderSync(device=device, gPodder=self)
2125 tracks = device.get_all_tracks()
2126 if len(tracks) > 0:
2127 remove_tracks_callback = lambda tracks: self.ipod_cleanup_callback(device, tracks)
2128 wanted_columns = []
2129 for key, sort_name, sort_type, caption in columns:
2130 want_this_column = False
2131 for track in tracks:
2132 if getattr(track, key) is not None:
2133 want_this_column = True
2134 break
2136 if want_this_column:
2137 wanted_columns.append((key, sort_name, sort_type, caption))
2138 title = _('Remove podcasts from device')
2139 instructions = _('Select the podcast episodes you want to remove from your device.')
2140 gPodderEpisodeSelector(title=title, instructions=instructions, episodes=tracks, columns=wanted_columns, \
2141 stock_ok_button=gtk.STOCK_DELETE, callback=remove_tracks_callback, tooltip_attribute=None)
2142 else:
2143 title = _('No files on device')
2144 message = _('The devices contains no files to be removed.')
2145 self.show_message(message, title)
2146 device.close()
2148 def on_manage_device_playlist(self, widget):
2149 # make sure gpod is available before even trying to sync
2150 if gl.config.device_type == 'ipod' and not sync.gpod_available:
2151 title = _('Cannot manage iPod playlist')
2152 message = _('This feature is not available for iPods.')
2153 self.notification( message, title )
2154 return
2155 elif gl.config.device_type == 'mtp' and not sync.pymtp_available:
2156 title = _('Cannot manage MTP device playlist')
2157 message = _('This feature is not available for MTP devices.')
2158 self.notification( message, title )
2159 return
2161 device = sync.open_device()
2163 if device is None:
2164 title = _('No device configured')
2165 message = _('To use the playlist feature, please configure your Filesystem based MP3-Player in the preferences dialog first.')
2166 self.notification(message, title)
2167 return
2169 if not device.open():
2170 title = _('Cannot open device')
2171 message = _('There has been an error opening your device.')
2172 self.notification(message, title)
2173 return
2175 gPodderPlaylist(device=device, gPodder=self)
2176 device.close()
2178 def show_hide_tray_icon(self):
2179 if gl.config.display_tray_icon and have_trayicon and self.tray_icon is None:
2180 self.tray_icon = trayicon.GPodderStatusIcon(self, scalable_dir)
2181 elif not gl.config.display_tray_icon and self.tray_icon is not None:
2182 self.tray_icon.set_visible(False)
2183 del self.tray_icon
2184 self.tray_icon = None
2186 if gl.config.minimize_to_tray and self.tray_icon:
2187 self.tray_icon.set_visible(self.minimized)
2188 elif self.tray_icon:
2189 self.tray_icon.set_visible(True)
2191 def on_itemShowToolbar_activate(self, widget):
2192 gl.config.show_toolbar = self.itemShowToolbar.get_active()
2194 def on_itemShowDescription_activate(self, widget):
2195 gl.config.episode_list_descriptions = self.itemShowDescription.get_active()
2197 def update_item_device( self):
2198 if gl.config.device_type != 'none':
2199 self.itemDevice.show_all()
2200 (label,) = self.itemDevice.get_children()
2201 label.set_text(gl.get_device_name())
2202 else:
2203 self.itemDevice.hide_all()
2205 def properties_closed( self):
2206 self.show_hide_tray_icon()
2207 self.update_item_device()
2208 self.updateComboBox()
2210 def on_itemPreferences_activate(self, widget, *args):
2211 if gpodder.interface == gpodder.GUI:
2212 gPodderProperties(callback_finished=self.properties_closed, user_apps_reader=self.user_apps_reader)
2213 else:
2214 gPodderMaemoPreferences()
2216 def on_itemDependencies_activate(self, widget):
2217 gPodderDependencyManager()
2219 def on_add_new_google_search(self, widget, *args):
2220 def add_google_video_search(query):
2221 self.add_new_channel('http://video.google.com/videofeed?type=search&q='+urllib.quote(query)+'&so=1&num=250&output=rss')
2223 gPodderAddPodcastDialog(url_callback=add_google_video_search, custom_title=_('Add Google Video search'), custom_label=_('Search for:'))
2225 def on_upgrade_from_videocenter(self, widget):
2226 from gpodder import nokiavideocenter
2227 vc = nokiavideocenter.UpgradeFromVideocenter()
2228 if vc.db2opml():
2229 gPodderOpmlLister(custom_title=_('Import podcasts from Video Center'), hide_url_entry=True).get_channels_from_url(vc.opmlfile, lambda url: self.add_new_channel(url,False,block=True), lambda: self.on_itemDownloadAllNew_activate(self.gPodder))
2230 else:
2231 self.show_message(_('Have you installed Video Center on your tablet?'), _('Cannot find Video Center subscriptions'))
2233 def require_my_gpodder_authentication(self):
2234 if not gl.config.my_gpodder_username or not gl.config.my_gpodder_password:
2235 success, authentication = self.UsernamePasswordDialog(_('Login to my.gpodder.org'), _('Please enter your e-mail address and your password.'), username=gl.config.my_gpodder_username, password=gl.config.my_gpodder_password, username_prompt=_('E-Mail Address'), register_callback=lambda: util.open_website('http://my.gpodder.org/register'))
2236 if success and authentication[0] and authentication[1]:
2237 gl.config.my_gpodder_username, gl.config.my_gpodder_password = authentication
2238 return True
2239 else:
2240 return False
2242 return True
2244 def my_gpodder_offer_autoupload(self):
2245 if not gl.config.my_gpodder_autoupload:
2246 if self.show_confirmation(_('gPodder can automatically upload your subscription list to my.gpodder.org when you close it. Do you want to enable this feature?'), _('Upload subscriptions on quit')):
2247 gl.config.my_gpodder_autoupload = True
2249 def on_download_from_mygpo(self, widget):
2250 if self.require_my_gpodder_authentication():
2251 client = my.MygPodderClient(gl.config.my_gpodder_username, gl.config.my_gpodder_password)
2252 opml_data = client.download_subscriptions()
2253 if len(opml_data) > 0:
2254 fp = open(gl.channel_opml_file, 'w')
2255 fp.write(opml_data)
2256 fp.close()
2257 (added, skipped) = (0, 0)
2258 i = opml.Importer(gl.channel_opml_file)
2259 for item in i.items:
2260 url = item['url']
2261 if url not in (c.url for c in self.channels):
2262 self.add_new_channel(url, ask_download_new=False, block=True)
2263 added += 1
2264 else:
2265 log('Already added: %s', url, sender=self)
2266 skipped += 1
2267 self.updateComboBox()
2268 if added > 0:
2269 self.show_message(_('Added %d new subscriptions and skipped %d existing ones.') % (added, skipped), _('Result of subscription download'))
2270 elif widget is not None:
2271 self.show_message(_('Your local subscription list is up to date.'), _('Result of subscription download'))
2272 self.my_gpodder_offer_autoupload()
2273 else:
2274 gl.config.my_gpodder_password = ''
2275 self.on_download_from_mygpo(widget)
2276 else:
2277 self.show_message(_('Please set up your username and password first.'), _('Username and password needed'))
2279 def on_upload_to_mygpo(self, widget):
2280 if self.require_my_gpodder_authentication():
2281 client = my.MygPodderClient(gl.config.my_gpodder_username, gl.config.my_gpodder_password)
2282 save_channels(self.channels)
2283 success, messages = client.upload_subscriptions(gl.channel_opml_file)
2284 if widget is not None:
2285 self.show_message('\n'.join(messages), _('Results of upload'))
2286 if not success:
2287 gl.config.my_gpodder_password = ''
2288 self.on_upload_to_mygpo(widget)
2289 else:
2290 self.my_gpodder_offer_autoupload()
2291 elif not success:
2292 log('Upload to my.gpodder.org failed, but widget is None!', sender=self)
2293 elif widget is not None:
2294 self.show_message(_('Please set up your username and password first.'), _('Username and password needed'))
2296 def on_itemAddChannel_activate(self, widget, *args):
2297 gPodderAddPodcastDialog(url_callback=self.add_new_channel)
2299 def on_itemEditChannel_activate(self, widget, *args):
2300 if self.active_channel is None:
2301 title = _('No podcast selected')
2302 message = _('Please select a podcast in the podcasts list to edit.')
2303 self.show_message( message, title)
2304 return
2306 gPodderChannel(channel=self.active_channel, callback_closed=lambda: self.updateComboBox(only_selected_channel=True), callback_change_url=self.change_channel_url)
2308 def change_channel_url(self, old_url, new_url):
2309 channel = None
2310 try:
2311 channel = podcastChannel.load(url=new_url, create=True)
2312 except:
2313 channel = None
2315 if channel is None:
2316 self.show_message(_('The specified URL is invalid. The old URL has been used instead.'), _('Invalid URL'))
2317 return
2319 for channel in self.channels:
2320 if channel.url == old_url:
2321 log('=> change channel url from %s to %s', old_url, new_url)
2322 old_save_dir = channel.save_dir
2323 channel.url = new_url
2324 new_save_dir = channel.save_dir
2325 log('old save dir=%s', old_save_dir, sender=self)
2326 log('new save dir=%s', new_save_dir, sender=self)
2327 files = glob.glob(os.path.join(old_save_dir, '*'))
2328 log('moving %d files to %s', len(files), new_save_dir, sender=self)
2329 for file in files:
2330 log('moving %s', file, sender=self)
2331 shutil.move(file, new_save_dir)
2332 try:
2333 os.rmdir(old_save_dir)
2334 except:
2335 log('Warning: cannot delete %s', old_save_dir, sender=self)
2337 save_channels(self.channels)
2338 # update feed cache and select the podcast with the new URL afterwards
2339 self.update_feed_cache(force_update=False, select_url_afterwards=new_url)
2341 def on_itemRemoveChannel_activate(self, widget, *args):
2342 try:
2343 if gpodder.interface == gpodder.GUI:
2344 dialog = gtk.MessageDialog(self.gPodder, gtk.DIALOG_MODAL, gtk.MESSAGE_QUESTION, gtk.BUTTONS_NONE)
2345 dialog.add_button(gtk.STOCK_NO, gtk.RESPONSE_NO)
2346 dialog.add_button(gtk.STOCK_YES, gtk.RESPONSE_YES)
2348 title = _('Remove podcast and episodes?')
2349 message = _('Do you really want to remove <b>%s</b> and all downloaded episodes?') % saxutils.escape(self.active_channel.title)
2351 dialog.set_title(title)
2352 dialog.set_markup('<span weight="bold" size="larger">%s</span>\n\n%s'%(title, message))
2354 cb_ask = gtk.CheckButton(_('Do not delete my downloaded episodes'))
2355 dialog.vbox.pack_start(cb_ask)
2356 cb_ask.show_all()
2357 affirmative = gtk.RESPONSE_YES
2358 elif gpodder.interface == gpodder.MAEMO:
2359 cb_ask = gtk.CheckButton('') # dummy check button
2360 dialog = hildon.Note('confirmation', (self.gPodder, _('Do you really want to remove this podcast and all downloaded episodes?')))
2361 affirmative = gtk.RESPONSE_OK
2363 result = dialog.run()
2364 dialog.destroy()
2366 if result == affirmative:
2367 # delete downloaded episodes only if checkbox is unchecked
2368 if cb_ask.get_active() == False:
2369 self.active_channel.remove_downloaded()
2370 else:
2371 log('Not removing downloaded episodes', sender=self)
2373 # only delete partial files if we do not have any downloads in progress
2374 delete_partial = not services.download_status_manager.has_items()
2375 gl.clean_up_downloads(delete_partial)
2377 # cancel any active downloads from this channel
2378 if not delete_partial:
2379 for episode in self.active_channel.get_all_episodes():
2380 services.download_status_manager.cancel_by_url(episode.url)
2382 # get the URL of the podcast we want to select next
2383 position = self.channels.index(self.active_channel)
2384 if position == len(self.channels)-1:
2385 # this is the last podcast, so select the URL
2386 # of the item before this one (i.e. the "new last")
2387 select_url = self.channels[position-1].url
2388 else:
2389 # there is a podcast after the deleted one, so
2390 # we simply select the one that comes after it
2391 select_url = self.channels[position+1].url
2393 # Remove the channel
2394 self.active_channel.delete()
2395 self.channels.remove(self.active_channel)
2396 self.channel_list_changed = True
2397 save_channels(self.channels)
2399 # Re-load the channels and select the desired new channel
2400 self.update_feed_cache(force_update=False, select_url_afterwards=select_url)
2401 except:
2402 log('There has been an error removing the channel.', traceback=True, sender=self)
2403 self.update_podcasts_tab()
2405 def get_opml_filter(self):
2406 filter = gtk.FileFilter()
2407 filter.add_pattern('*.opml')
2408 filter.add_pattern('*.xml')
2409 filter.set_name(_('OPML files')+' (*.opml, *.xml)')
2410 return filter
2412 def on_item_import_from_file_activate(self, widget, filename=None):
2413 if filename is None:
2414 if gpodder.interface == gpodder.GUI:
2415 dlg = gtk.FileChooserDialog(title=_('Import from OPML'), parent=None, action=gtk.FILE_CHOOSER_ACTION_OPEN)
2416 dlg.add_button(gtk.STOCK_CANCEL, gtk.RESPONSE_CANCEL)
2417 dlg.add_button(gtk.STOCK_OPEN, gtk.RESPONSE_OK)
2418 elif gpodder.interface == gpodder.MAEMO:
2419 dlg = hildon.FileChooserDialog(self.gPodder, gtk.FILE_CHOOSER_ACTION_OPEN)
2420 dlg.set_filter(self.get_opml_filter())
2421 response = dlg.run()
2422 filename = None
2423 if response == gtk.RESPONSE_OK:
2424 filename = dlg.get_filename()
2425 dlg.destroy()
2427 if filename is not None:
2428 gPodderOpmlLister(custom_title=_('Import podcasts from OPML file'), hide_url_entry=True).get_channels_from_url(filename, lambda url: self.add_new_channel(url,False,block=True), lambda: self.on_itemDownloadAllNew_activate(self.gPodder))
2430 def on_itemExportChannels_activate(self, widget, *args):
2431 if not self.channels:
2432 title = _('Nothing to export')
2433 message = _('Your list of podcast subscriptions is empty. Please subscribe to some podcasts first before trying to export your subscription list.')
2434 self.show_message( message, title)
2435 return
2437 if gpodder.interface == gpodder.GUI:
2438 dlg = gtk.FileChooserDialog(title=_('Export to OPML'), parent=self.gPodder, action=gtk.FILE_CHOOSER_ACTION_SAVE)
2439 dlg.add_button(gtk.STOCK_CANCEL, gtk.RESPONSE_CANCEL)
2440 dlg.add_button(gtk.STOCK_SAVE, gtk.RESPONSE_OK)
2441 elif gpodder.interface == gpodder.MAEMO:
2442 dlg = hildon.FileChooserDialog(self.gPodder, gtk.FILE_CHOOSER_ACTION_SAVE)
2443 dlg.set_filter(self.get_opml_filter())
2444 response = dlg.run()
2445 if response == gtk.RESPONSE_OK:
2446 filename = dlg.get_filename()
2447 dlg.destroy()
2448 exporter = opml.Exporter( filename)
2449 if exporter.write(self.channels):
2450 if len(self.channels) == 1:
2451 title = _('One subscription exported')
2452 else:
2453 title = _('%d subscriptions exported') % len(self.channels)
2454 self.show_message(_('Your podcast list has been successfully exported.'), title)
2455 else:
2456 self.show_message( _('Could not export OPML to file. Please check your permissions.'), _('OPML export failed'))
2457 else:
2458 dlg.destroy()
2460 def on_itemImportChannels_activate(self, widget, *args):
2461 gPodderOpmlLister().get_channels_from_url(gl.config.opml_url, lambda url: self.add_new_channel(url,False,block=True), lambda: self.on_itemDownloadAllNew_activate(self.gPodder))
2463 def on_homepage_activate(self, widget, *args):
2464 util.open_website(app_website)
2466 def on_wiki_activate(self, widget, *args):
2467 util.open_website('http://wiki.gpodder.org/')
2469 def on_bug_tracker_activate(self, widget, *args):
2470 util.open_website('http://bugs.gpodder.org/')
2472 def on_itemAbout_activate(self, widget, *args):
2473 dlg = gtk.AboutDialog()
2474 dlg.set_name(app_name.replace('p', 'P')) # gpodder->gPodder
2475 dlg.set_version( app_version)
2476 dlg.set_copyright( app_copyright)
2477 dlg.set_website( app_website)
2478 dlg.set_translator_credits( _('translator-credits'))
2479 dlg.connect( 'response', lambda dlg, response: dlg.destroy())
2481 if gpodder.interface == gpodder.GUI:
2482 # For the "GUI" version, we add some more
2483 # items to the about dialog (credits and logo)
2484 dlg.set_authors(app_authors)
2485 try:
2486 dlg.set_logo(gtk.gdk.pixbuf_new_from_file(scalable_dir))
2487 except:
2488 pass
2490 dlg.run()
2492 def on_wNotebook_switch_page(self, widget, *args):
2493 page_num = args[1]
2494 if gpodder.interface == gpodder.MAEMO:
2495 page = self.wNotebook.get_nth_page(page_num)
2496 tab_label = self.wNotebook.get_tab_label(page).get_text()
2497 if page_num == 0 and self.active_channel is not None:
2498 self.set_title(self.active_channel.title)
2499 else:
2500 self.set_title(tab_label)
2501 if page_num == 0:
2502 self.play_or_download()
2503 else:
2504 self.toolDownload.set_sensitive( False)
2505 self.toolPlay.set_sensitive( False)
2506 self.toolTransfer.set_sensitive( False)
2507 self.toolCancel.set_sensitive( services.download_status_manager.has_items())
2509 def on_treeChannels_row_activated(self, widget, *args):
2510 # double-click action of the podcast list
2511 pass
2513 def on_treeChannels_cursor_changed(self, widget, *args):
2514 ( model, iter ) = self.treeChannels.get_selection().get_selected()
2516 if model is not None and iter is not None:
2517 old_active_channel = self.active_channel
2518 (id,) = model.get_path(iter)
2519 self.active_channel = self.channels[id]
2521 if self.active_channel == old_active_channel:
2522 return
2524 if gpodder.interface == gpodder.MAEMO:
2525 self.set_title(self.active_channel.title)
2526 self.itemEditChannel.show_all()
2527 self.itemRemoveChannel.show_all()
2528 self.channel_toggle_lock.show_all()
2529 if self.active_channel.channel_is_locked:
2530 self.change_menu_item(self.channel_toggle_lock, gtk.STOCK_DIALOG_AUTHENTICATION, _('Allow deletion of all episodes'))
2531 else:
2532 self.change_menu_item(self.channel_toggle_lock, gtk.STOCK_DIALOG_AUTHENTICATION, _('Prohibit deletion of all episodes'))
2534 else:
2535 self.active_channel = None
2536 self.itemEditChannel.hide_all()
2537 self.itemRemoveChannel.hide_all()
2538 self.channel_toggle_lock.hide_all()
2540 self.updateTreeView()
2542 def on_entryAddChannel_changed(self, widget, *args):
2543 active = self.entryAddChannel.get_text() not in ('', self.ENTER_URL_TEXT)
2544 self.btnAddChannel.set_sensitive( active)
2546 def on_btnAddChannel_clicked(self, widget, *args):
2547 url = self.entryAddChannel.get_text()
2548 self.entryAddChannel.set_text('')
2549 self.add_new_channel( url)
2551 def on_btnEditChannel_clicked(self, widget, *args):
2552 self.on_itemEditChannel_activate( widget, args)
2554 def on_treeAvailable_row_activated(self, widget, path=None, view_column=None):
2556 What this function does depends on from which widget it is called.
2557 It gets the selected episodes of the current podcast and runs one
2558 of the following actions on them:
2560 * Transfer (to MP3 player, iPod, etc..)
2561 * Playback/open files
2562 * Show the episode info dialog
2563 * Download episodes
2565 try:
2566 selection = self.treeAvailable.get_selection()
2567 (model, paths) = selection.get_selected_rows()
2569 if len(paths) == 0:
2570 log('Nothing selected', sender=self)
2571 return
2573 wname = widget.get_name()
2574 do_transfer = (wname in ('itemTransferSelected', 'toolTransfer'))
2575 do_playback = (wname in ('itemPlaySelected', 'itemOpenSelected', 'toolPlay'))
2576 do_epdialog = (wname in ('treeAvailable', 'item_episode_details'))
2578 episodes = []
2579 for path in paths:
2580 it = model.get_iter(path)
2581 url = model.get_value(it, 0)
2582 episode = self.active_channel.find_episode(url)
2583 episodes.append(episode)
2585 if len(episodes) == 0:
2586 log('No episodes selected', sender=self)
2588 if do_transfer:
2589 self.on_sync_to_ipod_activate(widget, episodes)
2590 elif do_playback:
2591 for episode in episodes:
2592 # Make sure to mark the episode as downloaded
2593 if os.path.exists(episode.local_filename()):
2594 episode.channel.addDownloadedItem(episode)
2595 self.playback_episode(episode)
2596 elif gl.config.enable_streaming:
2597 self.playback_episode(episode, stream=True)
2598 elif do_epdialog:
2599 play_callback = lambda: self.playback_episode(episode)
2600 def download_callback():
2601 self.download_episode_list([episode])
2602 self.play_or_download()
2603 if self.gpodder_episode_window is None:
2604 log('First-time use of episode window --- creating', sender=self)
2605 self.gpodder_episode_window = gPodderEpisode()
2606 self.gpodder_episode_window.show(episode=episode, download_callback=download_callback, play_callback=play_callback)
2607 else:
2608 self.download_episode_list(episodes)
2609 self.play_or_download()
2610 except:
2611 log('Error in on_treeAvailable_row_activated', traceback=True, sender=self)
2613 def on_treeAvailable_button_release_event(self, widget, *args):
2614 self.play_or_download()
2616 def auto_update_procedure(self, first_run=False):
2617 log('auto_update_procedure() got called', sender=self)
2618 if not first_run and gl.config.auto_update_feeds and self.minimized:
2619 self.update_feed_cache(force_update=True)
2621 next_update = 60*1000*gl.config.auto_update_frequency
2622 gobject.timeout_add(next_update, self.auto_update_procedure)
2624 def on_treeDownloads_row_activated(self, widget, *args):
2625 cancel_urls = []
2627 if self.wNotebook.get_current_page() > 0:
2628 # Use the download list treeview + model
2629 ( tree, column ) = ( self.treeDownloads, 3 )
2630 else:
2631 # Use the available podcasts treeview + model
2632 ( tree, column ) = ( self.treeAvailable, 0 )
2634 selection = tree.get_selection()
2635 (model, paths) = selection.get_selected_rows()
2636 for path in paths:
2637 url = model.get_value( model.get_iter( path), column)
2638 cancel_urls.append( url)
2640 if len( cancel_urls) == 0:
2641 log('Nothing selected.', sender = self)
2642 return
2644 if len( cancel_urls) == 1:
2645 title = _('Cancel download?')
2646 message = _("Cancelling this download will remove the partially downloaded file and stop the download.")
2647 else:
2648 title = _('Cancel downloads?')
2649 message = _("Cancelling the download will stop the %d selected downloads and remove partially downloaded files.") % selection.count_selected_rows()
2651 if self.show_confirmation( message, title):
2652 services.download_status_manager.start_batch_mode()
2653 for url in cancel_urls:
2654 services.download_status_manager.cancel_by_url( url)
2655 services.download_status_manager.end_batch_mode()
2656 self.play_or_download()
2658 def on_btnCancelDownloadStatus_clicked(self, widget, *args):
2659 self.on_treeDownloads_row_activated( widget, None)
2661 def on_btnCancelAll_clicked(self, widget, *args):
2662 self.treeDownloads.get_selection().select_all()
2663 self.on_treeDownloads_row_activated( self.toolCancel, None)
2664 self.treeDownloads.get_selection().unselect_all()
2666 def on_btnDownloadedDelete_clicked(self, widget, *args):
2667 if self.active_channel is None:
2668 return
2670 channel_url = self.active_channel.url
2671 selection = self.treeAvailable.get_selection()
2672 ( model, paths ) = selection.get_selected_rows()
2674 if selection.count_selected_rows() == 0:
2675 log( 'Nothing selected - will not remove any downloaded episode.')
2676 return
2678 if selection.count_selected_rows() == 1:
2679 episode_title = saxutils.escape(model.get_value(model.get_iter(paths[0]), 1))
2681 episode = db.load_episode(model.get_value(model.get_iter(paths[0]), 0))
2682 if episode['is_locked']:
2683 title = _('%s is locked') % episode_title
2684 message = _('You cannot delete this locked episode. You must unlock it before you can delete it.')
2685 self.notification(message, title)
2686 return
2688 title = _('Remove %s?') % episode_title
2689 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.")
2690 else:
2691 title = _('Remove %d episodes?') % selection.count_selected_rows()
2692 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.')
2694 locked_count = 0
2695 for path in paths:
2696 episode = db.load_episode(model.get_value(model.get_iter(path), 0))
2697 if episode['is_locked']:
2698 locked_count += 1
2700 if selection.count_selected_rows() == locked_count:
2701 title = _('Episodes are locked')
2702 message = _('The selected episodes are locked. Please unlock the episodes that you want to delete before trying to delete them.')
2703 self.notification(message, title)
2704 return
2705 elif locked_count > 0:
2706 title = _('Remove %d out of %d episodes?') % (selection.count_selected_rows() - locked_count, selection.count_selected_rows())
2707 message = _('The selection contains locked episodes that will not be deleted. If you want to listen to the deleted episodes, you will have to re-download them.')
2709 # if user confirms deletion, let's remove some stuff ;)
2710 if self.show_confirmation( message, title):
2711 try:
2712 # iterate over the selection, see also on_treeDownloads_row_activated
2713 for path in paths:
2714 url = model.get_value( model.get_iter( path), 0)
2715 self.active_channel.delete_episode_by_url( url)
2717 # now, clear local db cache so we can re-read it
2718 self.updateComboBox()
2719 except:
2720 log( 'Error while deleting (some) downloads.')
2722 # only delete partial files if we do not have any downloads in progress
2723 delete_partial = not services.download_status_manager.has_items()
2724 gl.clean_up_downloads(delete_partial)
2725 self.update_selected_episode_list_icons()
2726 self.play_or_download()
2728 def on_key_press(self, widget, event):
2729 # Allow tab switching with Ctrl + PgUp/PgDown
2730 if event.state & gtk.gdk.CONTROL_MASK:
2731 if event.keyval == gtk.keysyms.Page_Up:
2732 self.wNotebook.prev_page()
2733 return True
2734 elif event.keyval == gtk.keysyms.Page_Down:
2735 self.wNotebook.next_page()
2736 return True
2738 # After this code we only handle Maemo hardware keys,
2739 # so if we are not a Maemo app, we don't do anything
2740 if gpodder.interface != gpodder.MAEMO:
2741 return False
2743 if event.keyval == gtk.keysyms.F6:
2744 if self.fullscreen:
2745 self.window.unfullscreen()
2746 else:
2747 self.window.fullscreen()
2748 if event.keyval == gtk.keysyms.Escape:
2749 new_visibility = not self.vboxChannelNavigator.get_property('visible')
2750 self.vboxChannelNavigator.set_property('visible', new_visibility)
2751 self.column_size.set_visible(not new_visibility)
2752 self.column_released.set_visible(not new_visibility)
2754 diff = 0
2755 if event.keyval == gtk.keysyms.F7: #plus
2756 diff = 1
2757 elif event.keyval == gtk.keysyms.F8: #minus
2758 diff = -1
2760 if diff != 0:
2761 selection = self.treeChannels.get_selection()
2762 (model, iter) = selection.get_selected()
2763 selection.select_path(((model.get_path(iter)[0]+diff)%len(model),))
2764 self.on_treeChannels_cursor_changed(self.treeChannels)
2766 def window_state_event(self, widget, event):
2767 if event.new_window_state & gtk.gdk.WINDOW_STATE_FULLSCREEN:
2768 self.fullscreen = True
2769 else:
2770 self.fullscreen = False
2772 old_minimized = self.minimized
2774 self.minimized = bool(event.new_window_state & gtk.gdk.WINDOW_STATE_ICONIFIED)
2775 if gpodder.interface == gpodder.MAEMO:
2776 self.minimized = bool(event.new_window_state & gtk.gdk.WINDOW_STATE_WITHDRAWN)
2778 if old_minimized != self.minimized and self.tray_icon:
2779 self.gPodder.set_skip_taskbar_hint(self.minimized)
2780 elif not self.tray_icon:
2781 self.gPodder.set_skip_taskbar_hint(False)
2783 if gl.config.minimize_to_tray and self.tray_icon:
2784 self.tray_icon.set_visible(self.minimized)
2786 def uniconify_main_window(self):
2787 if self.minimized:
2788 self.gPodder.present()
2790 def iconify_main_window(self):
2791 if not self.minimized:
2792 self.gPodder.iconify()
2794 def update_podcasts_tab(self):
2795 if len(self.channels):
2796 self.label2.set_text(_('Podcasts (%d)') % len(self.channels))
2797 else:
2798 self.label2.set_text(_('Podcasts'))
2800 class gPodderChannel(GladeWidget):
2801 finger_friendly_widgets = ['btn_website', 'btnOK', 'channel_description', 'label19', 'label37', 'label31']
2803 def new(self):
2804 global WEB_BROWSER_ICON
2805 self.changed = False
2806 self.image3167.set_property('icon-name', WEB_BROWSER_ICON)
2807 self.gPodderChannel.set_title( self.channel.title)
2808 self.entryTitle.set_text( self.channel.title)
2809 self.entryURL.set_text( self.channel.url)
2811 self.LabelDownloadTo.set_text( self.channel.save_dir)
2812 self.LabelWebsite.set_text( self.channel.link)
2814 self.cbNoSync.set_active( not self.channel.sync_to_devices)
2815 self.musicPlaylist.set_text(self.channel.device_playlist_name)
2816 if self.channel.username:
2817 self.FeedUsername.set_text( self.channel.username)
2818 if self.channel.password:
2819 self.FeedPassword.set_text( self.channel.password)
2821 services.cover_downloader.register('cover-available', self.cover_download_finished)
2822 services.cover_downloader.request_cover(self.channel)
2824 # Hide the website button if we don't have a valid URL
2825 if not self.channel.link:
2826 self.btn_website.hide_all()
2828 b = gtk.TextBuffer()
2829 b.set_text( self.channel.description)
2830 self.channel_description.set_buffer( b)
2832 #Add Drag and Drop Support
2833 flags = gtk.DEST_DEFAULT_ALL
2834 targets = [ ('text/uri-list', 0, 2), ('text/plain', 0, 4) ]
2835 actions = gtk.gdk.ACTION_DEFAULT | gtk.gdk.ACTION_COPY
2836 self.vboxCoverEditor.drag_dest_set( flags, targets, actions)
2837 self.vboxCoverEditor.connect( 'drag_data_received', self.drag_data_received)
2839 def on_btn_website_clicked(self, widget):
2840 util.open_website(self.channel.link)
2842 def on_btnDownloadCover_clicked(self, widget):
2843 if gpodder.interface == gpodder.GUI:
2844 dlg = gtk.FileChooserDialog(title=_('Select new podcast cover artwork'), parent=self.gPodderChannel, action=gtk.FILE_CHOOSER_ACTION_OPEN)
2845 dlg.add_button(gtk.STOCK_CANCEL, gtk.RESPONSE_CANCEL)
2846 dlg.add_button(gtk.STOCK_OPEN, gtk.RESPONSE_OK)
2847 elif gpodder.interface == gpodder.MAEMO:
2848 dlg = hildon.FileChooserDialog(self.gPodderChannel, gtk.FILE_CHOOSER_ACTION_OPEN)
2850 if dlg.run() == gtk.RESPONSE_OK:
2851 url = dlg.get_uri()
2852 services.cover_downloader.replace_cover(self.channel, url)
2854 dlg.destroy()
2856 def on_btnClearCover_clicked(self, widget):
2857 services.cover_downloader.replace_cover(self.channel)
2859 def cover_download_finished(self, channel_url, pixbuf):
2860 if pixbuf is not None:
2861 self.imgCover.set_from_pixbuf(pixbuf)
2862 self.gPodderChannel.show()
2864 def drag_data_received( self, widget, content, x, y, sel, ttype, time):
2865 files = sel.data.strip().split('\n')
2866 if len(files) != 1:
2867 self.show_message( _('You can only drop a single image or URL here.'), _('Drag and drop'))
2868 return
2870 file = files[0]
2872 if file.startswith('file://') or file.startswith('http://'):
2873 services.cover_downloader.replace_cover(self.channel, file)
2874 return
2876 self.show_message( _('You can only drop local files and http:// URLs here.'), _('Drag and drop'))
2878 def on_gPodderChannel_destroy(self, widget, *args):
2879 services.cover_downloader.unregister('cover-available', self.cover_download_finished)
2881 def on_btnOK_clicked(self, widget, *args):
2882 entered_url = self.entryURL.get_text()
2883 channel_url = self.channel.url
2885 if entered_url != channel_url:
2886 if self.show_confirmation(_('Do you really want to move this podcast to <b>%s</b>?') % (saxutils.escape(entered_url),), _('Really change URL?')):
2887 if hasattr(self, 'callback_change_url'):
2888 self.gPodderChannel.hide_all()
2889 self.callback_change_url(channel_url, entered_url)
2891 self.channel.sync_to_devices = not self.cbNoSync.get_active()
2892 self.channel.device_playlist_name = self.musicPlaylist.get_text()
2893 self.channel.set_custom_title( self.entryTitle.get_text())
2894 self.channel.username = self.FeedUsername.get_text().strip()
2895 self.channel.password = self.FeedPassword.get_text()
2896 self.channel.save()
2898 self.gPodderChannel.destroy()
2899 self.callback_closed()
2901 class gPodderAddPodcastDialog(GladeWidget):
2902 finger_friendly_widgets = ['btn_close', 'btn_add']
2904 def new(self):
2905 if not hasattr(self, 'url_callback'):
2906 log('No url callback set', sender=self)
2907 self.url_callback = None
2908 if hasattr(self, 'custom_label'):
2909 self.label_add.set_text(self.custom_label)
2910 if hasattr(self, 'custom_title'):
2911 self.gPodderAddPodcastDialog.set_title(self.custom_title)
2912 if gpodder.interface == gpodder.MAEMO:
2913 self.entry_url.set_text('http://')
2915 def on_btn_close_clicked(self, widget):
2916 self.gPodderAddPodcastDialog.destroy()
2918 def on_entry_url_changed(self, widget):
2919 self.btn_add.set_sensitive(self.entry_url.get_text().strip() != '')
2921 def on_btn_add_clicked(self, widget):
2922 url = self.entry_url.get_text()
2923 self.on_btn_close_clicked(widget)
2924 if self.url_callback is not None:
2925 self.url_callback(url)
2928 class gPodderMaemoPreferences(GladeWidget):
2929 finger_friendly_widgets = ['btn_close', 'label128', 'label129', 'btn_advanced']
2931 def new(self):
2932 gl.config.connect_gtk_togglebutton('update_on_startup', self.update_on_startup)
2933 gl.config.connect_gtk_togglebutton('display_tray_icon', self.show_tray_icon)
2934 gl.config.connect_gtk_togglebutton('enable_notifications', self.show_notifications)
2935 gl.config.connect_gtk_togglebutton('on_quit_ask', self.on_quit_ask)
2937 self.restart_required = False
2938 self.show_tray_icon.connect('clicked', self.on_restart_required)
2939 self.show_notifications.connect('clicked', self.on_restart_required)
2941 def on_restart_required(self, widget):
2942 self.restart_required = True
2944 def on_btn_advanced_clicked(self, widget):
2945 self.gPodderMaemoPreferences.destroy()
2946 gPodderConfigEditor()
2948 def on_btn_close_clicked(self, widget):
2949 self.gPodderMaemoPreferences.destroy()
2950 if self.restart_required:
2951 self.show_message(_('Please restart gPodder for the changes to take effect.'))
2954 class gPodderProperties(GladeWidget):
2955 def new(self):
2956 if not hasattr( self, 'callback_finished'):
2957 self.callback_finished = None
2959 if gpodder.interface == gpodder.MAEMO:
2960 self.table5.hide_all() # player
2961 self.gPodderProperties.fullscreen()
2963 gl.config.connect_gtk_editable( 'http_proxy', self.httpProxy)
2964 gl.config.connect_gtk_editable( 'ftp_proxy', self.ftpProxy)
2965 gl.config.connect_gtk_editable( 'player', self.openApp)
2966 gl.config.connect_gtk_editable('videoplayer', self.openVideoApp)
2967 gl.config.connect_gtk_editable( 'custom_sync_name', self.entryCustomSyncName)
2968 gl.config.connect_gtk_togglebutton( 'custom_sync_name_enabled', self.cbCustomSyncName)
2969 gl.config.connect_gtk_togglebutton( 'auto_download_when_minimized', self.downloadnew)
2970 gl.config.connect_gtk_togglebutton( 'update_on_startup', self.updateonstartup)
2971 gl.config.connect_gtk_togglebutton( 'only_sync_not_played', self.only_sync_not_played)
2972 gl.config.connect_gtk_togglebutton( 'fssync_channel_subfolders', self.cbChannelSubfolder)
2973 gl.config.connect_gtk_togglebutton( 'on_sync_mark_played', self.on_sync_mark_played)
2974 gl.config.connect_gtk_togglebutton( 'on_sync_delete', self.on_sync_delete)
2975 gl.config.connect_gtk_togglebutton( 'proxy_use_environment', self.cbEnvironmentVariables)
2976 gl.config.connect_gtk_spinbutton('episode_old_age', self.episode_old_age)
2977 gl.config.connect_gtk_togglebutton('auto_remove_old_episodes', self.auto_remove_old_episodes)
2978 gl.config.connect_gtk_togglebutton('auto_update_feeds', self.auto_update_feeds)
2979 gl.config.connect_gtk_spinbutton('auto_update_frequency', self.auto_update_frequency)
2980 gl.config.connect_gtk_togglebutton('display_tray_icon', self.display_tray_icon)
2981 gl.config.connect_gtk_togglebutton('minimize_to_tray', self.minimize_to_tray)
2982 gl.config.connect_gtk_togglebutton('enable_notifications', self.enable_notifications)
2983 gl.config.connect_gtk_togglebutton('start_iconified', self.start_iconified)
2984 gl.config.connect_gtk_togglebutton('ipod_write_gtkpod_extended', self.ipod_write_gtkpod_extended)
2985 gl.config.connect_gtk_togglebutton('mp3_player_delete_played', self.delete_episodes_marked_played)
2987 self.enable_notifications.set_sensitive(self.display_tray_icon.get_active())
2988 self.minimize_to_tray.set_sensitive(self.display_tray_icon.get_active())
2990 self.entryCustomSyncName.set_sensitive( self.cbCustomSyncName.get_active())
2992 self.iPodMountpoint.set_label( gl.config.ipod_mount)
2993 self.filesystemMountpoint.set_label( gl.config.mp3_player_folder)
2994 self.chooserDownloadTo.set_current_folder(gl.downloaddir)
2996 self.on_sync_delete.set_sensitive(not self.delete_episodes_marked_played.get_active())
2997 self.on_sync_mark_played.set_sensitive(not self.delete_episodes_marked_played.get_active())
2999 if tagging_supported():
3000 gl.config.connect_gtk_togglebutton( 'update_tags', self.updatetags)
3001 else:
3002 self.updatetags.set_sensitive( False)
3003 new_label = '%s (%s)' % ( self.updatetags.get_label(), _('needs python-eyed3') )
3004 self.updatetags.set_label( new_label)
3006 # device type
3007 self.comboboxDeviceType.set_active( 0)
3008 if gl.config.device_type == 'ipod':
3009 self.comboboxDeviceType.set_active( 1)
3010 elif gl.config.device_type == 'filesystem':
3011 self.comboboxDeviceType.set_active( 2)
3012 elif gl.config.device_type == 'mtp':
3013 self.comboboxDeviceType.set_active( 3)
3015 # setup cell renderers
3016 cellrenderer = gtk.CellRendererPixbuf()
3017 self.comboAudioPlayerApp.pack_start(cellrenderer, False)
3018 self.comboAudioPlayerApp.add_attribute(cellrenderer, 'pixbuf', 2)
3019 cellrenderer = gtk.CellRendererText()
3020 self.comboAudioPlayerApp.pack_start(cellrenderer, True)
3021 self.comboAudioPlayerApp.add_attribute(cellrenderer, 'markup', 0)
3023 cellrenderer = gtk.CellRendererPixbuf()
3024 self.comboVideoPlayerApp.pack_start(cellrenderer, False)
3025 self.comboVideoPlayerApp.add_attribute(cellrenderer, 'pixbuf', 2)
3026 cellrenderer = gtk.CellRendererText()
3027 self.comboVideoPlayerApp.pack_start(cellrenderer, True)
3028 self.comboVideoPlayerApp.add_attribute(cellrenderer, 'markup', 0)
3030 if not hasattr(self, 'user_apps_reader'):
3031 self.user_apps_reader = UserAppsReader(['audio', 'video'])
3033 self.comboAudioPlayerApp.set_row_separator_func(self.is_row_separator)
3034 self.comboVideoPlayerApp.set_row_separator_func(self.is_row_separator)
3036 if gpodder.interface == gpodder.GUI:
3037 self.user_apps_reader.read()
3039 self.comboAudioPlayerApp.set_model(self.user_apps_reader.get_applications_as_model('audio'))
3040 index = self.find_active_audio_app()
3041 self.comboAudioPlayerApp.set_active(index)
3042 self.comboVideoPlayerApp.set_model(self.user_apps_reader.get_applications_as_model('video'))
3043 index = self.find_active_video_app()
3044 self.comboVideoPlayerApp.set_active(index)
3046 self.ipodIcon.set_from_icon_name( 'gnome-dev-ipod', gtk.ICON_SIZE_BUTTON)
3048 def is_row_separator(self, model, iter):
3049 return model.get_value(iter, 0) == ''
3051 def update_mountpoint( self, ipod):
3052 if ipod is None or ipod.mount_point is None:
3053 self.iPodMountpoint.set_label( '')
3054 else:
3055 self.iPodMountpoint.set_label( ipod.mount_point)
3057 def find_active_audio_app(self):
3058 index_custom = -1
3059 model = self.comboAudioPlayerApp.get_model()
3060 iter = model.get_iter_first()
3061 index = 0
3062 while iter is not None:
3063 command = model.get_value(iter, 1)
3064 if command == self.openApp.get_text():
3065 return index
3066 if index_custom < 0 and command == '':
3067 index_custom = index
3068 iter = model.iter_next(iter)
3069 index += 1
3070 # return index of custom command or first item
3071 return max(0, index_custom)
3073 def find_active_video_app( self):
3074 index_custom = -1
3075 model = self.comboVideoPlayerApp.get_model()
3076 iter = model.get_iter_first()
3077 index = 0
3078 while iter is not None:
3079 command = model.get_value(iter, 1)
3080 if command == self.openVideoApp.get_text():
3081 return index
3082 if index_custom < 0 and command == '':
3083 index_custom = index
3084 iter = model.iter_next(iter)
3085 index += 1
3086 # return index of custom command or first item
3087 return max(0, index_custom)
3089 def set_download_dir( self, new_download_dir, event = None):
3090 gl.downloaddir = self.chooserDownloadTo.get_filename()
3091 if gl.downloaddir != self.chooserDownloadTo.get_filename():
3092 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'))
3094 if event:
3095 event.set()
3097 def on_auto_update_feeds_toggled( self, widget, *args):
3098 self.auto_update_frequency.set_sensitive(widget.get_active())
3100 def on_display_tray_icon_toggled( self, widget, *args):
3101 self.enable_notifications.set_sensitive(widget.get_active())
3102 self.minimize_to_tray.set_sensitive(widget.get_active())
3104 def on_cbCustomSyncName_toggled( self, widget, *args):
3105 self.entryCustomSyncName.set_sensitive( widget.get_active())
3107 def on_only_sync_not_played_toggled( self, widget, *args):
3108 self.delete_episodes_marked_played.set_sensitive( widget.get_active())
3109 if not widget.get_active():
3110 self.delete_episodes_marked_played.set_active(False)
3112 def on_delete_episodes_marked_played_toggled( self, widget, *args):
3113 if widget.get_active() and self.only_sync_not_played.get_active():
3114 self.on_sync_leave.set_active(True)
3115 self.on_sync_delete.set_sensitive(not widget.get_active())
3116 self.on_sync_mark_played.set_sensitive(not widget.get_active())
3118 def on_btnCustomSyncNameHelp_clicked( self, widget):
3119 examples = [
3120 '<i>{episode.title}</i> -&gt; <b>Interview with RMS</b>',
3121 '<i>{episode.basename}</i> -&gt; <b>70908-interview-rms</b>',
3122 '<i>{episode.published}</i> -&gt; <b>20070908</b> (for 08.09.2007)',
3123 '<i>{episode.pubtime}</i> -&gt; <b>1344</b> (for 13:44)',
3124 '<i>{podcast.title}</i> -&gt; <b>The Interview Podcast</b>'
3127 info = [
3128 _('You can specify a custom format string for the file names on your MP3 player here.'),
3129 _('The format string will be used to generate a file name on your device. The file extension (e.g. ".mp3") will be added automatically.'),
3130 '\n'.join( [ ' %s' % s for s in examples ])
3133 self.show_message( '\n\n'.join( info), _('Custom format strings'))
3135 def on_gPodderProperties_destroy(self, widget, *args):
3136 self.on_btnOK_clicked( widget, *args)
3138 def on_btnConfigEditor_clicked(self, widget, *args):
3139 self.on_btnOK_clicked(widget, *args)
3140 gPodderConfigEditor()
3142 def on_comboAudioPlayerApp_changed(self, widget, *args):
3143 # find out which one
3144 iter = self.comboAudioPlayerApp.get_active_iter()
3145 model = self.comboAudioPlayerApp.get_model()
3146 command = model.get_value( iter, 1)
3147 if command == '':
3148 if self.openApp.get_text() == 'default':
3149 self.openApp.set_text('')
3150 self.openApp.set_sensitive( True)
3151 self.openApp.show()
3152 self.labelCustomCommand.show()
3153 else:
3154 self.openApp.set_text( command)
3155 self.openApp.set_sensitive( False)
3156 self.openApp.hide()
3157 self.labelCustomCommand.hide()
3159 def on_comboVideoPlayerApp_changed(self, widget, *args):
3160 # find out which one
3161 iter = self.comboVideoPlayerApp.get_active_iter()
3162 model = self.comboVideoPlayerApp.get_model()
3163 command = model.get_value(iter, 1)
3164 if command == '':
3165 if self.openVideoApp.get_text() == 'default':
3166 self.openVideoApp.set_text('')
3167 self.openVideoApp.set_sensitive(True)
3168 self.openVideoApp.show()
3169 self.labelCustomVideoCommand.show()
3170 else:
3171 self.openVideoApp.set_text(command)
3172 self.openVideoApp.set_sensitive(False)
3173 self.openVideoApp.hide()
3174 self.labelCustomVideoCommand.hide()
3176 def on_cbEnvironmentVariables_toggled(self, widget, *args):
3177 sens = not self.cbEnvironmentVariables.get_active()
3178 self.httpProxy.set_sensitive( sens)
3179 self.ftpProxy.set_sensitive( sens)
3181 def on_comboboxDeviceType_changed(self, widget, *args):
3182 active_item = self.comboboxDeviceType.get_active()
3184 # None
3185 sync_widgets = ( self.only_sync_not_played, self.labelSyncOptions,
3186 self.imageSyncOptions, self. separatorSyncOptions,
3187 self.on_sync_mark_played, self.on_sync_delete,
3188 self.on_sync_leave, self.label_after_sync, self.delete_episodes_marked_played)
3189 for widget in sync_widgets:
3190 if active_item == 0:
3191 widget.hide_all()
3192 else:
3193 widget.show_all()
3195 # iPod
3196 ipod_widgets = (self.ipodLabel, self.btn_iPodMountpoint,
3197 self.ipod_write_gtkpod_extended)
3198 for widget in ipod_widgets:
3199 if active_item == 1:
3200 widget.show_all()
3201 else:
3202 widget.hide_all()
3204 # filesystem-based MP3 player
3205 fs_widgets = ( self.filesystemLabel, self.btn_filesystemMountpoint,
3206 self.cbChannelSubfolder, self.cbCustomSyncName,
3207 self.entryCustomSyncName, self.btnCustomSyncNameHelp )
3208 for widget in fs_widgets:
3209 if active_item == 2:
3210 widget.show_all()
3211 else:
3212 widget.hide_all()
3214 def on_btn_iPodMountpoint_clicked(self, widget, *args):
3215 fs = gtk.FileChooserDialog( title = _('Select iPod mountpoint'), action = gtk.FILE_CHOOSER_ACTION_SELECT_FOLDER)
3216 fs.add_button( gtk.STOCK_CANCEL, gtk.RESPONSE_CANCEL)
3217 fs.add_button( gtk.STOCK_OPEN, gtk.RESPONSE_OK)
3218 fs.set_current_folder(self.iPodMountpoint.get_label())
3219 if fs.run() == gtk.RESPONSE_OK:
3220 self.iPodMountpoint.set_label( fs.get_filename())
3221 fs.destroy()
3223 def on_btn_FilesystemMountpoint_clicked(self, widget, *args):
3224 fs = gtk.FileChooserDialog( title = _('Select folder for MP3 player'), action = gtk.FILE_CHOOSER_ACTION_SELECT_FOLDER)
3225 fs.add_button( gtk.STOCK_CANCEL, gtk.RESPONSE_CANCEL)
3226 fs.add_button( gtk.STOCK_OPEN, gtk.RESPONSE_OK)
3227 fs.set_current_folder(self.filesystemMountpoint.get_label())
3228 if fs.run() == gtk.RESPONSE_OK:
3229 self.filesystemMountpoint.set_label( fs.get_filename())
3230 fs.destroy()
3232 def on_btnOK_clicked(self, widget, *args):
3233 gl.config.ipod_mount = self.iPodMountpoint.get_label()
3234 gl.config.mp3_player_folder = self.filesystemMountpoint.get_label()
3236 if gl.downloaddir != self.chooserDownloadTo.get_filename():
3237 new_download_dir = self.chooserDownloadTo.get_filename()
3238 download_dir_size = util.calculate_size( gl.downloaddir)
3239 download_dir_size_string = gl.format_filesize( download_dir_size)
3240 event = Event()
3242 dlg = gtk.Dialog( _('Moving downloads folder'), self.gPodderProperties)
3243 dlg.vbox.set_spacing( 5)
3244 dlg.set_border_width( 5)
3246 label = gtk.Label()
3247 label.set_line_wrap( True)
3248 label.set_markup( _('Moving downloads from <b>%s</b> to <b>%s</b>...') % ( saxutils.escape( gl.downloaddir), saxutils.escape( new_download_dir), ))
3249 myprogressbar = gtk.ProgressBar()
3251 # put it all together
3252 dlg.vbox.pack_start( label)
3253 dlg.vbox.pack_end( myprogressbar)
3255 # switch windows
3256 dlg.show_all()
3257 self.gPodderProperties.hide_all()
3259 # hide action area and separator line
3260 dlg.action_area.hide()
3261 dlg.set_has_separator( False)
3263 args = ( new_download_dir, event, )
3265 thread = Thread( target = self.set_download_dir, args = args)
3266 thread.start()
3268 while not event.isSet():
3269 try:
3270 new_download_dir_size = util.calculate_size( new_download_dir)
3271 except:
3272 new_download_dir_size = 0
3273 if download_dir_size > 0:
3274 fract = (1.00*new_download_dir_size) / (1.00*download_dir_size)
3275 else:
3276 fract = 0.0
3277 if fract < 0.99:
3278 myprogressbar.set_text( _('%s of %s') % ( gl.format_filesize( new_download_dir_size), download_dir_size_string, ))
3279 else:
3280 myprogressbar.set_text( _('Finishing... please wait.'))
3281 myprogressbar.set_fraction(max(0.0,min(1.0,fract)))
3282 event.wait( 0.1)
3283 while gtk.events_pending():
3284 gtk.main_iteration( False)
3286 dlg.destroy()
3288 device_type = self.comboboxDeviceType.get_active()
3289 if device_type == 0:
3290 gl.config.device_type = 'none'
3291 elif device_type == 1:
3292 gl.config.device_type = 'ipod'
3293 elif device_type == 2:
3294 gl.config.device_type = 'filesystem'
3295 elif device_type == 3:
3296 gl.config.device_type = 'mtp'
3297 self.gPodderProperties.destroy()
3298 if self.callback_finished:
3299 self.callback_finished()
3302 class gPodderEpisode(GladeWidget):
3303 finger_friendly_widgets = ['btnPlay', 'btnDownload', 'btnCancel', 'btnClose', 'textview']
3305 def new(self):
3306 setattr(self, 'episode', None)
3307 setattr(self, 'download_callback', None)
3308 setattr(self, 'play_callback', None)
3309 self.gPodderEpisode.connect('delete-event', self.on_delete_event)
3310 gl.config.connect_gtk_window(self.gPodderEpisode, 'episode_window', True)
3311 services.download_status_manager.register('list-changed', self.on_download_status_changed)
3312 services.download_status_manager.register('progress-detail', self.on_download_status_progress)
3313 self.textview.modify_bg(gtk.STATE_NORMAL, gtk.gdk.color_parse('#ffffff'))
3314 if gl.config.enable_html_shownotes:
3315 try:
3316 import gtkhtml2
3317 setattr(self, 'have_gtkhtml2', True)
3318 # Generate a HTML view and remove the textview
3319 setattr(self, 'htmlview', gtkhtml2.View())
3320 self.scrolled_window.remove(self.scrolled_window.get_child())
3321 self.scrolled_window.add(self.htmlview)
3322 self.textview = None
3323 self.htmlview.set_document(gtkhtml2.Document())
3324 self.htmlview.show()
3325 except ImportError:
3326 log('Install gtkhtml2 if you want HTML shownotes', sender=self)
3327 setattr(self, 'have_gtkhtml2', False)
3328 else:
3329 setattr(self, 'have_gtkhtml2', False)
3331 def show(self, episode, download_callback, play_callback):
3332 self.episode = episode
3333 self.download_callback = download_callback
3334 self.play_callback = play_callback
3336 self.gPodderEpisode.set_title(self.episode.title)
3338 if self.have_gtkhtml2:
3339 import gtkhtml2
3340 d = gtkhtml2.Document()
3341 d.open_stream('text/html')
3342 d.write_stream('<html><head></head><body><em>%s</em></body></html>' % _('Loading shownotes...'))
3343 d.close_stream()
3344 self.htmlview.set_document(d)
3345 else:
3346 b = gtk.TextBuffer()
3347 self.textview.set_buffer(b)
3349 self.hide_show_widgets()
3350 self.gPodderEpisode.show()
3352 # Make sure the window comes up right now:
3353 while gtk.events_pending():
3354 gtk.main_iteration(False)
3356 # Now do the stuff that takes a bit longer...
3357 heading = self.episode.title
3358 subheading = 'from %s' % (self.episode.channel.title)
3359 description = self.episode.description
3360 footer = []
3362 if self.have_gtkhtml2:
3363 import gtkhtml2
3364 d.connect('link-clicked', lambda d, url: util.open_website(url))
3365 def request_url(document, url, stream):
3366 def opendata(url, stream):
3367 fp = urllib2.urlopen(url)
3368 data = fp.read(1024*10)
3369 while data != '':
3370 stream.write(data)
3371 data = fp.read(1024*10)
3372 stream.close()
3373 Thread(target=opendata, args=[url, stream]).start()
3374 d.connect('request-url', request_url)
3375 d.clear()
3376 d.open_stream('text/html')
3377 d.write_stream('<html><head><meta http-equiv="content-type" content="text/html; charset=utf-8"/></head><body>')
3378 d.write_stream('<span style="font-size: big; font-weight: bold;">%s</span><br><span style="font-size: small;">%s</span><hr style="border: 1px #eeeeee solid;"><p>' % (saxutils.escape(heading), saxutils.escape(subheading)))
3379 d.write_stream(self.episode.description)
3380 if len(footer):
3381 d.write_stream('<hr style="border: 1px #eeeeee solid;">')
3382 d.write_stream('<span style="font-size: small;">%s</span>' % ('<br>'.join(((saxutils.escape(f) for f in footer))),))
3383 d.write_stream('</p></body></html>')
3384 d.close_stream()
3385 else:
3386 b.create_tag('heading', scale=pango.SCALE_LARGE, weight=pango.WEIGHT_BOLD)
3387 b.create_tag('subheading', scale=pango.SCALE_SMALL)
3388 b.create_tag('footer', scale=pango.SCALE_SMALL)
3390 b.insert_with_tags_by_name(b.get_end_iter(), heading, 'heading')
3391 b.insert_at_cursor('\n')
3392 b.insert_with_tags_by_name(b.get_end_iter(), subheading, 'subheading')
3393 b.insert_at_cursor('\n\n')
3394 b.insert(b.get_end_iter(), util.remove_html_tags(description))
3395 if len(footer):
3396 b.insert_at_cursor('\n\n')
3397 b.insert_with_tags_by_name(b.get_end_iter(), '\n'.join(footer), 'footer')
3399 services.download_status_manager.request_progress_detail(self.episode.url)
3401 def on_cancel(self, widget):
3402 services.download_status_manager.cancel_by_url(self.episode.url)
3404 def on_delete_event(self, widget, event):
3405 # Avoid destroying the dialog, simply hide
3406 self.on_close(widget)
3407 return True
3409 def on_close(self, widget):
3410 self.episode = None
3411 if self.have_gtkhtml2:
3412 import gtkhtml2
3413 self.htmlview.set_document(gtkhtml2.Document())
3414 else:
3415 self.textview.get_buffer().set_text('')
3416 self.gPodderEpisode.hide()
3418 def on_download_status_changed(self, episode_urls, channel_urls):
3419 if self.gPodderEpisode.get_property('visible'):
3420 self.hide_show_widgets()
3421 else:
3422 log('download status changed, but not visible', sender=self)
3424 def on_download_status_progress(self, url, progress, speed):
3425 if self.episode is None:
3426 return
3428 if url == self.episode.url:
3429 progress = float(min(100.0,max(0.0,progress)))
3430 self.download_progress.set_fraction(progress/100.0)
3431 self.download_progress.set_text('Downloading: %d%% (%s)' % (progress, speed))
3433 def hide_show_widgets(self):
3434 is_downloading = services.download_status_manager.is_download_in_progress(self.episode.url)
3435 if is_downloading:
3436 self.download_progress.show_all()
3437 self.btnCancel.show_all()
3438 self.btnPlay.hide_all()
3439 self.btnDownload.hide_all()
3440 else:
3441 self.download_progress.hide_all()
3442 self.btnCancel.hide_all()
3443 if os.path.exists(self.episode.local_filename()):
3444 if self.episode.file_type() in ('audio', 'video'):
3445 self.btnPlay.set_label(gtk.STOCK_MEDIA_PLAY)
3446 else:
3447 self.btnPlay.set_label(gtk.STOCK_OPEN)
3448 self.btnPlay.set_use_stock(True)
3449 self.btnPlay.show_all()
3450 self.btnDownload.hide_all()
3451 else:
3452 self.btnPlay.hide_all()
3453 self.btnDownload.show_all()
3455 def on_download(self, widget):
3456 if self.download_callback:
3457 self.download_callback()
3459 def on_playback(self, widget):
3460 if self.play_callback:
3461 self.play_callback()
3462 self.on_close(widget)
3464 class gPodderSync(GladeWidget):
3465 def new(self):
3466 util.idle_add(self.imageSync.set_from_icon_name, 'gnome-dev-ipod', gtk.ICON_SIZE_DIALOG)
3468 self.device.register('progress', self.on_progress)
3469 self.device.register('sub-progress', self.on_sub_progress)
3470 self.device.register('status', self.on_status)
3471 self.device.register('done', self.on_done)
3473 def on_progress(self, pos, max, text=None):
3474 if text is None:
3475 text = _('%d of %d done') % (pos, max)
3476 util.idle_add(self.progressbar.set_fraction, float(pos)/float(max))
3477 util.idle_add(self.progressbar.set_text, text)
3479 def on_sub_progress(self, percentage):
3480 util.idle_add(self.progressbar.set_text, _('Processing (%d%%)') % (percentage))
3482 def on_status(self, status):
3483 util.idle_add(self.status_label.set_markup, '<i>%s</i>' % saxutils.escape(status))
3485 def on_done(self):
3486 util.idle_add(self.gPodderSync.destroy)
3487 if not self.gPodder.minimized:
3488 util.idle_add(self.notification, _('Your device has been updated by gPodder.'), _('Operation finished'))
3490 def on_gPodderSync_destroy(self, widget, *args):
3491 self.device.unregister('progress', self.on_progress)
3492 self.device.unregister('sub-progress', self.on_sub_progress)
3493 self.device.unregister('status', self.on_status)
3494 self.device.unregister('done', self.on_done)
3495 self.device.cancel()
3497 def on_cancel_button_clicked(self, widget, *args):
3498 self.device.cancel()
3501 class gPodderOpmlLister(GladeWidget):
3502 finger_friendly_widgets = ['btnDownloadOpml', 'btnCancel', 'btnOK', 'treeviewChannelChooser']
3504 def new(self):
3505 # initiate channels list
3506 self.channels = []
3507 self.callback_for_channel = None
3508 self.callback_finished = None
3510 if hasattr(self, 'custom_title'):
3511 self.gPodderOpmlLister.set_title(self.custom_title)
3512 if hasattr(self, 'hide_url_entry'):
3513 self.hboxOpmlUrlEntry.hide_all()
3514 new_parent = self.notebookChannelAdder.get_parent()
3515 new_parent.remove(self.notebookChannelAdder)
3516 self.vboxOpmlImport.reparent(new_parent)
3518 self.setup_treeview(self.treeviewChannelChooser)
3519 self.setup_treeview(self.treeviewTopPodcastsChooser)
3520 self.setup_treeview(self.treeviewYouTubeChooser)
3522 self.notebookChannelAdder.connect('switch-page', lambda a, b, c: self.on_change_tab(c))
3524 def setup_treeview(self, tv):
3525 togglecell = gtk.CellRendererToggle()
3526 togglecell.set_property( 'activatable', True)
3527 togglecell.connect( 'toggled', self.callback_edited)
3528 togglecolumn = gtk.TreeViewColumn( '', togglecell, active=0)
3530 titlecell = gtk.CellRendererText()
3531 titlecell.set_property('ellipsize', pango.ELLIPSIZE_END)
3532 titlecolumn = gtk.TreeViewColumn(_('Podcast'), titlecell, markup=1)
3534 for itemcolumn in ( togglecolumn, titlecolumn ):
3535 tv.append_column(itemcolumn)
3537 def callback_edited( self, cell, path):
3538 model = self.get_treeview().get_model()
3540 url = model[path][2]
3542 model[path][0] = not model[path][0]
3543 if model[path][0]:
3544 self.channels.append( url)
3545 else:
3546 self.channels.remove( url)
3548 self.btnOK.set_sensitive( bool(len(self.get_selected_channels())))
3550 def get_selected_channels(self, tab=None):
3551 channels = []
3553 model = self.get_treeview(tab).get_model()
3554 if model is not None:
3555 for row in model:
3556 if row[0]:
3557 channels.append(row[2])
3559 return channels
3561 def on_change_tab(self, tab):
3562 self.btnOK.set_sensitive( bool(len(self.get_selected_channels(tab))))
3564 def thread_finished(self, model, tab=0):
3565 if tab == 1:
3566 tv = self.treeviewTopPodcastsChooser
3567 elif tab == 2:
3568 tv = self.treeviewYouTubeChooser
3569 self.entryYoutubeSearch.set_sensitive(True)
3570 self.btnSearchYouTube.set_sensitive(True)
3571 self.btnOK.set_sensitive(False)
3572 else:
3573 tv = self.treeviewChannelChooser
3574 self.btnDownloadOpml.set_sensitive(True)
3575 self.entryURL.set_sensitive(True)
3576 self.channels = []
3578 tv.set_model(model)
3579 tv.set_sensitive(True)
3581 def thread_func(self, tab=0):
3582 if tab == 1:
3583 model = opml.Importer(gl.config.toplist_url).get_model()
3584 if len(model) == 0:
3585 self.notification(_('The specified URL does not provide any valid OPML podcast items.'), _('No feeds found'))
3586 elif tab == 2:
3587 model = resolver.find_youtube_channels(self.entryYoutubeSearch.get_text())
3588 if len(model) == 0:
3589 self.notification(_('There are no YouTube channels that would match this query.'), _('No channels found'))
3590 else:
3591 model = opml.Importer(self.entryURL.get_text()).get_model()
3592 if len(model) == 0:
3593 self.notification(_('The specified URL does not provide any valid OPML podcast items.'), _('No feeds found'))
3595 util.idle_add(self.thread_finished, model, tab)
3597 def get_channels_from_url( self, url, callback_for_channel = None, callback_finished = None):
3598 if callback_for_channel:
3599 self.callback_for_channel = callback_for_channel
3600 if callback_finished:
3601 self.callback_finished = callback_finished
3602 self.entryURL.set_text( url)
3603 self.btnDownloadOpml.set_sensitive( False)
3604 self.entryURL.set_sensitive( False)
3605 self.btnOK.set_sensitive( False)
3606 self.treeviewChannelChooser.set_sensitive( False)
3607 Thread( target = self.thread_func).start()
3608 Thread( target = lambda: self.thread_func(1)).start()
3610 def select_all( self, value ):
3611 enabled = False
3612 model = self.get_treeview().get_model()
3613 if model is not None:
3614 for row in model:
3615 row[0] = value
3616 if value:
3617 enabled = True
3618 self.btnOK.set_sensitive(enabled)
3620 def on_gPodderOpmlLister_destroy(self, widget, *args):
3621 pass
3623 def on_btnDownloadOpml_clicked(self, widget, *args):
3624 self.get_channels_from_url( self.entryURL.get_text())
3626 def on_btnSearchYouTube_clicked(self, widget, *args):
3627 self.entryYoutubeSearch.set_sensitive(False)
3628 self.treeviewYouTubeChooser.set_sensitive(False)
3629 self.btnSearchYouTube.set_sensitive(False)
3630 Thread(target = lambda: self.thread_func(2)).start()
3632 def on_btnSelectAll_clicked(self, widget, *args):
3633 self.select_all(True)
3635 def on_btnSelectNone_clicked(self, widget, *args):
3636 self.select_all(False)
3638 def on_btnOK_clicked(self, widget, *args):
3639 self.channels = self.get_selected_channels()
3640 self.gPodderOpmlLister.destroy()
3642 # add channels that have been selected
3643 for url in self.channels:
3644 if self.callback_for_channel:
3645 self.callback_for_channel( url)
3647 if self.callback_finished:
3648 util.idle_add(self.callback_finished)
3650 def on_btnCancel_clicked(self, widget, *args):
3651 self.gPodderOpmlLister.destroy()
3653 def on_entryYoutubeSearch_key_press_event(self, widget, event):
3654 if event.keyval == gtk.keysyms.Return:
3655 self.on_btnSearchYouTube_clicked(widget)
3657 def get_treeview(self, tab=None):
3658 if tab is None:
3659 tab = self.notebookChannelAdder.get_current_page()
3661 if tab == 0:
3662 return self.treeviewChannelChooser
3663 elif tab == 1:
3664 return self.treeviewTopPodcastsChooser
3665 else:
3666 return self.treeviewYouTubeChooser
3668 class gPodderEpisodeSelector( GladeWidget):
3669 """Episode selection dialog
3671 Optional keyword arguments that modify the behaviour of this dialog:
3673 - callback: Function that takes 1 parameter which is a list of
3674 the selected episodes (or empty list when none selected)
3675 - remove_callback: Function that takes 1 parameter which is a list
3676 of episodes that should be "removed" (see below)
3677 (default is None, which means remove not possible)
3678 - remove_action: Label for the "remove" action (default is "Remove")
3679 - remove_finished: Callback after all remove callbacks have finished
3680 (default is None, also depends on remove_callback)
3681 It will get a list of episode URLs that have been
3682 removed, so the main UI can update those
3683 - episodes: List of episodes that are presented for selection
3684 - selected: (optional) List of boolean variables that define the
3685 default checked state for the given episodes
3686 - selected_default: (optional) The default boolean value for the
3687 checked state if no other value is set
3688 (default is False)
3689 - columns: List of (name, sort_name, sort_type, caption) pairs for the
3690 columns, the name is the attribute name of the episode to be
3691 read from each episode object. The sort name is the
3692 attribute name of the episode to be used to sort this column.
3693 If the sort_name is None it will use the attribute name for
3694 sorting. The sort type is the type of the sort column.
3695 The caption attribute is the text that appear as column caption
3696 (default is [('title_and_description', None, None, 'Episode'),])
3697 - title: (optional) The title of the window + heading
3698 - instructions: (optional) A one-line text describing what the
3699 user should select / what the selection is for
3700 - stock_ok_button: (optional) Will replace the "OK" button with
3701 another GTK+ stock item to be used for the
3702 affirmative button of the dialog (e.g. can
3703 be gtk.STOCK_DELETE when the episodes to be
3704 selected will be deleted after closing the
3705 dialog)
3706 - selection_buttons: (optional) A dictionary with labels as
3707 keys and callbacks as values; for each
3708 key a button will be generated, and when
3709 the button is clicked, the callback will
3710 be called for each episode and the return
3711 value of the callback (True or False) will
3712 be the new selected state of the episode
3713 - size_attribute: (optional) The name of an attribute of the
3714 supplied episode objects that can be used to
3715 calculate the size of an episode; set this to
3716 None if no total size calculation should be
3717 done (in cases where total size is useless)
3718 (default is 'length')
3719 - tooltip_attribute: (optional) The name of an attribute of
3720 the supplied episode objects that holds
3721 the text for the tooltips when hovering
3722 over an episode (default is 'description')
3725 finger_friendly_widgets = ['btnCancel', 'btnOK', 'btnCheckAll', 'btnCheckNone', 'treeviewEpisodes']
3727 COLUMN_INDEX = 0
3728 COLUMN_TOOLTIP = 1
3729 COLUMN_TOGGLE = 2
3730 COLUMN_ADDITIONAL = 3
3732 def new( self):
3733 gl.config.connect_gtk_window(self.gPodderEpisodeSelector, 'episode_selector', True)
3734 if not hasattr( self, 'callback'):
3735 self.callback = None
3737 if not hasattr(self, 'remove_callback'):
3738 self.remove_callback = None
3740 if not hasattr(self, 'remove_action'):
3741 self.remove_action = _('Remove')
3743 if not hasattr(self, 'remove_finished'):
3744 self.remove_finished = None
3746 if not hasattr( self, 'episodes'):
3747 self.episodes = []
3749 if not hasattr( self, 'size_attribute'):
3750 self.size_attribute = 'length'
3752 if not hasattr(self, 'tooltip_attribute'):
3753 self.tooltip_attribute = 'description'
3755 if not hasattr( self, 'selection_buttons'):
3756 self.selection_buttons = {}
3758 if not hasattr( self, 'selected_default'):
3759 self.selected_default = False
3761 if not hasattr( self, 'selected'):
3762 self.selected = [self.selected_default]*len(self.episodes)
3764 if len(self.selected) < len(self.episodes):
3765 self.selected += [self.selected_default]*(len(self.episodes)-len(self.selected))
3767 if not hasattr( self, 'columns'):
3768 self.columns = (('title_and_description', None, None, _('Episode')),)
3770 if hasattr( self, 'title'):
3771 self.gPodderEpisodeSelector.set_title( self.title)
3772 self.labelHeading.set_markup( '<b><big>%s</big></b>' % saxutils.escape( self.title))
3774 if gpodder.interface == gpodder.MAEMO:
3775 self.labelHeading.hide()
3777 if hasattr( self, 'instructions'):
3778 self.labelInstructions.set_text( self.instructions)
3779 self.labelInstructions.show_all()
3781 if hasattr(self, 'stock_ok_button'):
3782 if self.stock_ok_button == 'gpodder-download':
3783 self.btnOK.set_image(gtk.image_new_from_stock(gtk.STOCK_GO_DOWN, gtk.ICON_SIZE_BUTTON))
3784 self.btnOK.set_label(_('Download'))
3785 else:
3786 self.btnOK.set_label(self.stock_ok_button)
3787 self.btnOK.set_use_stock(True)
3789 # check/uncheck column
3790 toggle_cell = gtk.CellRendererToggle()
3791 toggle_cell.connect( 'toggled', self.toggle_cell_handler)
3792 self.treeviewEpisodes.append_column( gtk.TreeViewColumn( '', toggle_cell, active=self.COLUMN_TOGGLE))
3794 next_column = self.COLUMN_ADDITIONAL
3795 for name, sort_name, sort_type, caption in self.columns:
3796 renderer = gtk.CellRendererText()
3797 renderer.set_property( 'ellipsize', pango.ELLIPSIZE_END)
3798 column = gtk.TreeViewColumn(caption, renderer, markup=next_column)
3799 column.set_resizable( True)
3800 # Only set "expand" on the first column (so more text is displayed there)
3801 column.set_expand(next_column == self.COLUMN_ADDITIONAL)
3802 if sort_name is not None:
3803 column.set_sort_column_id(next_column+1)
3804 else:
3805 column.set_sort_column_id(next_column)
3806 self.treeviewEpisodes.append_column( column)
3807 next_column += 1
3809 if sort_name is not None:
3810 # add the sort column
3811 column = gtk.TreeViewColumn()
3812 column.set_visible(False)
3813 self.treeviewEpisodes.append_column( column)
3814 next_column += 1
3816 column_types = [ gobject.TYPE_INT, gobject.TYPE_STRING, gobject.TYPE_BOOLEAN ]
3817 # add string column type plus sort column type if it exists
3818 for name, sort_name, sort_type, caption in self.columns:
3819 column_types.append(gobject.TYPE_STRING)
3820 if sort_name is not None:
3821 column_types.append(sort_type)
3822 self.model = gtk.ListStore( *column_types)
3824 tooltip = None
3825 for index, episode in enumerate( self.episodes):
3826 if self.tooltip_attribute is not None:
3827 try:
3828 tooltip = getattr(episode, self.tooltip_attribute)
3829 except:
3830 log('Episode object %s does not have tooltip attribute: "%s"', episode, self.tooltip_attribute, sender=self)
3831 tooltip = None
3832 row = [ index, tooltip, self.selected[index] ]
3833 for name, sort_name, sort_type, caption in self.columns:
3834 if not hasattr(episode, name):
3835 log('Warning: Missing attribute "%s"', name, sender=self)
3836 row.append(None)
3837 else:
3838 row.append(getattr( episode, name))
3840 if sort_name is not None:
3841 if not hasattr(episode, sort_name):
3842 log('Warning: Missing attribute "%s"', sort_name, sender=self)
3843 row.append(None)
3844 else:
3845 row.append(getattr( episode, sort_name))
3846 self.model.append( row)
3848 if self.remove_callback is not None:
3849 self.btnRemoveAction.show()
3850 self.btnRemoveAction.set_label(self.remove_action)
3852 # connect to tooltip signals
3853 if self.tooltip_attribute is not None:
3854 try:
3855 self.treeviewEpisodes.set_property('has-tooltip', True)
3856 self.treeviewEpisodes.connect('query-tooltip', self.treeview_episodes_query_tooltip)
3857 except:
3858 log('I cannot set has-tooltip/query-tooltip (need at least PyGTK 2.12)', sender=self)
3859 self.last_tooltip_episode = None
3860 self.episode_list_can_tooltip = True
3862 self.treeviewEpisodes.connect('button-press-event', self.treeview_episodes_button_pressed)
3863 self.treeviewEpisodes.set_rules_hint( True)
3864 self.treeviewEpisodes.set_model( self.model)
3865 self.treeviewEpisodes.columns_autosize()
3866 self.calculate_total_size()
3868 def treeview_episodes_query_tooltip(self, treeview, x, y, keyboard_tooltip, tooltip):
3869 # With get_bin_window, we get the window that contains the rows without
3870 # the header. The Y coordinate of this window will be the height of the
3871 # treeview header. This is the amount we have to subtract from the
3872 # event's Y coordinate to get the coordinate to pass to get_path_at_pos
3873 (x_bin, y_bin) = treeview.get_bin_window().get_position()
3874 y -= x_bin
3875 y -= y_bin
3876 (path, column, rx, ry) = treeview.get_path_at_pos(x, y) or (None,)*4
3878 if not self.episode_list_can_tooltip:
3879 self.last_tooltip_episode = None
3880 return False
3882 if path is not None:
3883 model = treeview.get_model()
3884 iter = model.get_iter(path)
3885 index = model.get_value(iter, self.COLUMN_INDEX)
3886 description = model.get_value(iter, self.COLUMN_TOOLTIP)
3887 if self.last_tooltip_episode is not None and self.last_tooltip_episode != index:
3888 self.last_tooltip_episode = None
3889 return False
3890 self.last_tooltip_episode = index
3892 if description is not None:
3893 tooltip.set_text(description)
3894 return True
3895 else:
3896 return False
3898 self.last_tooltip_episode = None
3899 return False
3901 def treeview_episodes_button_pressed(self, treeview, event):
3902 if event.button == 3:
3903 menu = gtk.Menu()
3905 if len(self.selection_buttons):
3906 for label in self.selection_buttons:
3907 item = gtk.MenuItem(label)
3908 item.connect('activate', self.custom_selection_button_clicked, label)
3909 menu.append(item)
3910 menu.append(gtk.SeparatorMenuItem())
3912 item = gtk.MenuItem(_('Select all'))
3913 item.connect('activate', self.on_btnCheckAll_clicked)
3914 menu.append(item)
3916 item = gtk.MenuItem(_('Select none'))
3917 item.connect('activate', self.on_btnCheckNone_clicked)
3918 menu.append(item)
3920 menu.show_all()
3921 # Disable tooltips while we are showing the menu, so
3922 # the tooltip will not appear over the menu
3923 self.episode_list_can_tooltip = False
3924 menu.connect('deactivate', lambda menushell: self.episode_list_allow_tooltips())
3925 menu.popup(None, None, None, event.button, event.time)
3927 return True
3929 def episode_list_allow_tooltips(self):
3930 self.episode_list_can_tooltip = True
3932 def calculate_total_size( self):
3933 if self.size_attribute is not None:
3934 (total_size, count) = (0, 0)
3935 for episode in self.get_selected_episodes():
3936 try:
3937 total_size += int(getattr( episode, self.size_attribute))
3938 count += 1
3939 except:
3940 log( 'Cannot get size for %s', episode.title, sender = self)
3942 text = []
3943 if count == 0:
3944 text.append(_('Nothing selected'))
3945 elif count == 1:
3946 text.append(_('One episode selected'))
3947 else:
3948 text.append(_('%d episodes selected') % count)
3949 if total_size > 0:
3950 text.append(_('total size: %s') % gl.format_filesize(total_size))
3951 self.labelTotalSize.set_text(', '.join(text))
3952 self.btnOK.set_sensitive(count>0)
3953 self.btnRemoveAction.set_sensitive(count>0)
3954 if count > 0:
3955 self.btnCancel.set_label(gtk.STOCK_CANCEL)
3956 else:
3957 self.btnCancel.set_label(gtk.STOCK_CLOSE)
3958 else:
3959 self.btnOK.set_sensitive(False)
3960 self.btnRemoveAction.set_sensitive(False)
3961 for index, row in enumerate(self.model):
3962 if self.model.get_value(row.iter, self.COLUMN_TOGGLE) == True:
3963 self.btnOK.set_sensitive(True)
3964 self.btnRemoveAction.set_sensitive(True)
3965 break
3966 self.labelTotalSize.set_text('')
3968 def toggle_cell_handler( self, cell, path):
3969 model = self.treeviewEpisodes.get_model()
3970 model[path][self.COLUMN_TOGGLE] = not model[path][self.COLUMN_TOGGLE]
3972 self.calculate_total_size()
3974 def custom_selection_button_clicked(self, button, label):
3975 callback = self.selection_buttons[label]
3977 for index, row in enumerate( self.model):
3978 new_value = callback( self.episodes[index])
3979 self.model.set_value( row.iter, self.COLUMN_TOGGLE, new_value)
3981 self.calculate_total_size()
3983 def on_btnCheckAll_clicked( self, widget):
3984 for row in self.model:
3985 self.model.set_value( row.iter, self.COLUMN_TOGGLE, True)
3987 self.calculate_total_size()
3989 def on_btnCheckNone_clicked( self, widget):
3990 for row in self.model:
3991 self.model.set_value( row.iter, self.COLUMN_TOGGLE, False)
3993 self.calculate_total_size()
3995 def on_remove_action_activate(self, widget):
3996 episodes = self.get_selected_episodes(remove_episodes=True)
3998 urls = []
3999 for episode in episodes:
4000 urls.append(episode.url)
4001 self.remove_callback(episode)
4003 if self.remove_finished is not None:
4004 self.remove_finished(urls)
4005 self.calculate_total_size()
4007 def get_selected_episodes( self, remove_episodes=False):
4008 selected_episodes = []
4010 for index, row in enumerate( self.model):
4011 if self.model.get_value( row.iter, self.COLUMN_TOGGLE) == True:
4012 selected_episodes.append( self.episodes[self.model.get_value( row.iter, self.COLUMN_INDEX)])
4014 if remove_episodes:
4015 for episode in selected_episodes:
4016 index = self.episodes.index(episode)
4017 iter = self.model.get_iter_first()
4018 while iter is not None:
4019 if self.model.get_value(iter, self.COLUMN_INDEX) == index:
4020 self.model.remove(iter)
4021 break
4022 iter = self.model.iter_next(iter)
4024 return selected_episodes
4026 def on_btnOK_clicked( self, widget):
4027 self.gPodderEpisodeSelector.destroy()
4028 if self.callback is not None:
4029 self.callback( self.get_selected_episodes())
4031 def on_btnCancel_clicked( self, widget):
4032 self.gPodderEpisodeSelector.destroy()
4033 if self.callback is not None:
4034 self.callback([])
4036 class gPodderConfigEditor(GladeWidget):
4037 finger_friendly_widgets = ['btnShowAll', 'btnClose', 'configeditor']
4039 def new(self):
4040 name_column = gtk.TreeViewColumn(_('Setting'))
4041 name_renderer = gtk.CellRendererText()
4042 name_column.pack_start(name_renderer)
4043 name_column.add_attribute(name_renderer, 'text', 0)
4044 name_column.add_attribute(name_renderer, 'style', 5)
4045 self.configeditor.append_column(name_column)
4047 value_column = gtk.TreeViewColumn(_('Set to'))
4048 value_check_renderer = gtk.CellRendererToggle()
4049 value_column.pack_start(value_check_renderer, expand=False)
4050 value_column.add_attribute(value_check_renderer, 'active', 7)
4051 value_column.add_attribute(value_check_renderer, 'visible', 6)
4052 value_column.add_attribute(value_check_renderer, 'activatable', 6)
4053 value_check_renderer.connect('toggled', self.value_toggled)
4055 value_renderer = gtk.CellRendererText()
4056 value_column.pack_start(value_renderer)
4057 value_column.add_attribute(value_renderer, 'text', 2)
4058 value_column.add_attribute(value_renderer, 'visible', 4)
4059 value_column.add_attribute(value_renderer, 'editable', 4)
4060 value_column.add_attribute(value_renderer, 'style', 5)
4061 value_renderer.connect('edited', self.value_edited)
4062 self.configeditor.append_column(value_column)
4064 self.model = gl.config.model()
4065 self.filter = self.model.filter_new()
4066 self.filter.set_visible_func(self.visible_func)
4068 self.configeditor.set_model(self.filter)
4069 self.configeditor.set_rules_hint(True)
4071 def visible_func(self, model, iter, user_data=None):
4072 text = self.entryFilter.get_text().lower()
4073 if text == '':
4074 return True
4075 else:
4076 # either the variable name or its value
4077 return (text in model.get_value(iter, 0).lower() or
4078 text in model.get_value(iter, 2).lower())
4080 def value_edited(self, renderer, path, new_text):
4081 model = self.configeditor.get_model()
4082 iter = model.get_iter(path)
4083 name = model.get_value(iter, 0)
4084 type_cute = model.get_value(iter, 1)
4086 if not gl.config.update_field(name, new_text):
4087 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))
4089 def value_toggled(self, renderer, path):
4090 model = self.configeditor.get_model()
4091 iter = model.get_iter(path)
4092 field_name = model.get_value(iter, 0)
4093 field_type = model.get_value(iter, 3)
4095 # Flip the boolean config flag
4096 if field_type == bool:
4097 gl.config.toggle_flag(field_name)
4099 def on_entryFilter_changed(self, widget):
4100 self.filter.refilter()
4102 def on_btnShowAll_clicked(self, widget):
4103 self.entryFilter.set_text('')
4104 self.entryFilter.grab_focus()
4106 def on_btnClose_clicked(self, widget):
4107 self.gPodderConfigEditor.destroy()
4109 class gPodderPlaylist(GladeWidget):
4110 finger_friendly_widgets = ['btnCancelPlaylist', 'btnSavePlaylist', 'treeviewPlaylist']
4112 def new(self):
4113 self.m3u_header = '#EXTM3U\n'
4114 self.mountpoint = util.find_mount_point(gl.config.mp3_player_folder)
4115 if self.mountpoint == '/':
4116 self.mountpoint = gl.config.mp3_player_folder
4117 log('Warning: MP3 player resides on / - using %s as MP3 player root', self.mountpoint, sender=self)
4118 self.playlist_file = os.path.join(self.mountpoint,
4119 gl.config.mp3_player_playlist_file)
4120 icon_theme = gtk.icon_theme_get_default()
4121 self.icon_new = icon_theme.load_icon(gtk.STOCK_NEW, 16, 0)
4123 # add column two
4124 check_cell = gtk.CellRendererToggle()
4125 check_cell.set_property('activatable', True)
4126 check_cell.connect('toggled', self.cell_toggled)
4127 check_column = gtk.TreeViewColumn(_('Use'), check_cell, active=1)
4128 self.treeviewPlaylist.append_column(check_column)
4130 # add column three
4131 column = gtk.TreeViewColumn(_('Filename'))
4132 icon_cell = gtk.CellRendererPixbuf()
4133 column.pack_start(icon_cell, False)
4134 column.add_attribute(icon_cell, 'pixbuf', 0)
4135 filename_cell = gtk.CellRendererText()
4136 column.pack_start(filename_cell, True)
4137 column.add_attribute(filename_cell, 'text', 2)
4139 column.set_resizable(True)
4140 self.treeviewPlaylist.append_column(column)
4142 # Make treeview reorderable
4143 self.treeviewPlaylist.set_reorderable(True)
4145 # init liststore
4146 self.playlist = gtk.ListStore(gtk.gdk.Pixbuf, bool, str)
4147 self.treeviewPlaylist.set_model(self.playlist)
4149 # read device and playlist and fill the TreeView
4150 self.m3u = self.read_m3u()
4151 self.device = self.read_device()
4152 self.write2gui()
4154 def cell_toggled(self, cellrenderertoggle, path):
4155 (treeview, liststore) = (self.treeviewPlaylist, self.playlist)
4156 it = liststore.get_iter(path)
4157 liststore.set_value(it, 1, not liststore.get_value(it, 1))
4159 def on_btnCancelPlaylist_clicked(self, widget):
4160 self.gPodderPlaylist.destroy()
4162 def on_btnSavePlaylist_clicked(self, widget):
4163 self.write_m3u()
4164 self.gPodderPlaylist.destroy()
4166 def read_m3u(self):
4168 read all files from the existing playlist
4170 tracks = []
4171 if os.path.exists(self.playlist_file):
4172 for line in open(self.playlist_file, 'r'):
4173 if line != self.m3u_header:
4174 if line.startswith('#'):
4175 tracks.append([False, line[1:].strip()])
4176 else:
4177 tracks.append([True, line.strip()])
4178 return tracks
4180 def write_m3u(self):
4182 write the list into the playlist on the device
4184 playlist_folder = os.path.split(self.playlist_file)[0]
4185 if not util.make_directory(playlist_folder):
4186 self.show_message(_('Folder %s could not be created.') % playlist_folder, _('Error writing playlist'))
4187 else:
4188 try:
4189 fp = open(self.playlist_file, 'w')
4190 fp.write(self.m3u_header)
4191 for icon, checked, filename in self.playlist:
4192 if not checked:
4193 fp.write('#')
4194 fp.write(filename)
4195 fp.write('\n')
4196 fp.close()
4197 self.show_message(_('The playlist on your MP3 player has been updated.'), _('Update successful'))
4198 except IOError, ioe:
4199 self.show_message(str(ioe), _('Error writing playlist file'))
4201 def read_device(self):
4203 read all files from the device
4205 tracks = []
4206 for root, dirs, files in os.walk(gl.config.mp3_player_folder):
4207 for file in files:
4208 filename = os.path.join(root, file)
4210 if filename == self.playlist_file:
4211 # We don't want to have our playlist file as
4212 # an entry in our file list, so skip it!
4213 break
4215 if not gl.config.mp3_player_playlist_absolute_path:
4216 filename = filename[len(self.mountpoint):]
4218 if gl.config.mp3_player_playlist_win_path:
4219 filename = filename.replace( '/', '\\')
4221 tracks.append(filename)
4222 return tracks
4224 def write2gui(self):
4225 # add the files from the device to the list only when
4226 # they are not yet in the playlist
4227 # mark this files as NEW
4228 for filename in self.device[:]:
4229 m3ulist = [file[1] for file in self.m3u]
4230 if filename not in m3ulist:
4231 self.playlist.append([self.icon_new, False, filename])
4233 # add the files from the playlist to the list only when
4234 # they are on the device
4235 for checked, filename in self.m3u[:]:
4236 if filename in self.device:
4237 self.playlist.append([None, checked, filename])
4239 class gPodderDependencyManager(GladeWidget):
4240 def new(self):
4241 col_name = gtk.TreeViewColumn(_('Feature'), gtk.CellRendererText(), text=0)
4242 self.treeview_components.append_column(col_name)
4243 col_installed = gtk.TreeViewColumn(_('Status'), gtk.CellRendererText(), text=2)
4244 self.treeview_components.append_column(col_installed)
4245 self.treeview_components.set_model(services.dependency_manager.get_model())
4246 self.btn_about.set_sensitive(False)
4248 def on_btn_about_clicked(self, widget):
4249 selection = self.treeview_components.get_selection()
4250 model, iter = selection.get_selected()
4251 if iter is not None:
4252 title = model.get_value(iter, 0)
4253 description = model.get_value(iter, 1)
4254 available = model.get_value(iter, 3)
4255 missing = model.get_value(iter, 4)
4257 if not available:
4258 description += '\n\n'+_('Missing components:')+'\n\n'+missing
4260 self.show_message(description, title)
4262 def on_btn_install_clicked(self, widget):
4263 # TODO: Implement package manager integration
4264 pass
4266 def on_treeview_components_cursor_changed(self, treeview):
4267 self.btn_about.set_sensitive(treeview.get_selection().count_selected_rows() > 0)
4268 # TODO: If installing is possible, enable btn_install
4270 def on_gPodderDependencyManager_response(self, dialog, response_id):
4271 self.gPodderDependencyManager.destroy()
4273 class gPodderWelcome(GladeWidget):
4274 finger_friendly_widgets = ['btnOPML', 'btnMygPodder', 'btnCancel']
4276 def new(self):
4277 pass
4279 def on_show_example_podcasts(self, button):
4280 self.gPodderWelcome.destroy()
4281 self.show_example_podcasts_callback(None)
4283 def on_setup_my_gpodder(self, gpodder):
4284 self.gPodderWelcome.destroy()
4285 self.setup_my_gpodder_callback(None)
4287 def on_btnCancel_clicked(self, button):
4288 self.gPodderWelcome.destroy()
4290 def main():
4291 gobject.threads_init()
4292 gtk.window_set_default_icon_name( 'gpodder')
4294 gPodder().run()