Tue, 01 Jul 2008 22:46:52 -0400 <me@nikosapi.org>
[gpodder.git] / src / gpodder / gui.py
blobdfbb87b316866d9f1c4bad42d08409da23899aad
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 urllib2
31 import datetime
33 from xml.sax import saxutils
35 from threading import Event
36 from threading import Thread
37 from string import strip
39 import gpodder
40 from gpodder import util
41 from gpodder import opml
42 from gpodder import services
43 from gpodder import sync
44 from gpodder import download
45 from gpodder import SimpleGladeApp
46 from gpodder.liblogger import log
47 from gpodder.dbsqlite import db
49 try:
50 from gpodder import trayicon
51 have_trayicon = True
52 except Exception, exc:
53 log('Warning: Could not import gpodder.trayicon.', traceback=True)
54 log('Warning: This probably means your PyGTK installation is too old!')
55 have_trayicon = False
57 from libpodcasts import podcastChannel
58 from libpodcasts import LocalDBReader
59 from libpodcasts import podcastItem
60 from libpodcasts import channels_to_model
61 from libpodcasts import load_channels
62 from libpodcasts import update_channels
63 from libpodcasts import save_channels
64 from libpodcasts import can_restore_from_opml
66 from gpodder.libgpodder import gl
68 from libplayers import UserAppsReader
70 from libtagupdate import tagging_supported
72 if gpodder.interface == gpodder.GUI:
73 WEB_BROWSER_ICON = 'web-browser'
74 elif gpodder.interface == gpodder.MAEMO:
75 import hildon
76 WEB_BROWSER_ICON = 'qgn_toolb_browser_web'
78 app_name = "gpodder"
79 app_version = "unknown" # will be set in main() call
80 app_authors = [
81 _('Current maintainer:'), 'Thomas Perl <thpinfo.com>',
82 '',
83 _('Patches, bug reports and donations by:'), 'Adrien Beaucreux',
84 'Alain Tauch', 'Alistair Sutton', 'Anders Kvist', 'Andy Busch',
85 'Antonio Roversi', 'Aravind Seshadri', 'Atte André Jensen',
86 'Bernd Schlapsi', 'Bill Barnard', 'Bjørn Rasmussen', 'Camille Moncelier',
87 'Carlos Moffat', 'Chris', 'Chris Arnold', 'Clark Burbidge', 'Daniel Ramos',
88 'David Spreen', 'Doug Hellmann', 'FFranci72', 'Florian Richter', 'Frank Harper',
89 'FriedBunny', 'Gerrit Sangel', 'Götz Waschk',
90 'Haim Roitgrund', 'Hex', 'Holger Bauer', 'Holger Leskien', 'Jens Thiele',
91 'Jérôme Chabod', 'Jerry Moss',
92 'Jessica Henline', 'João Trindade', 'Joel Calado', 'John Ferguson',
93 'José Luis Fustel', 'Joseph Bleau', 'Julio Acuña', 'Junio C Hamano',
94 'Jürgen Schinker', 'Justin Forest',
95 'Konstantin Ryabitsev', 'Leonid Ponomarev', 'Marcos Hernández', 'Mark Alford', 'Michael Salim',
96 'Mika Leppinen', 'Mike Coulson', 'Mykola Nikishov', 'narf at inode.at',
97 'Nick L.', 'Nicolas Quienot', 'Ondrej Vesely',
98 'Ortwin Forster', 'Paul Elliot', 'Paul Rudkin',
99 'Pavel Mlčoch', 'Peter Hoffmann', 'Philippe Gouaillier', 'Pieter de Decker',
100 'Preben Randhol', 'Rafael Proença', 'red26wings', 'Richard Voigt',
101 'Robert Young', 'Roel Groeneveld',
102 'Scott Wegner', 'Sebastian Krause', 'Seth Remington', 'Shane Donohoe', 'SPGoetze',
103 'Stefan Lohmaier', 'Stephan Buys', 'Stylianos Papanastasiou', 'Teo Ramirez',
104 'Thomas Matthijs', 'Thomas Mills Hinkle', 'Thomas Nilsson',
105 'Tim Michelsen', 'Tim Preetz', 'Todd Zullinger', 'Tomas Matheson', 'VladDrac',
106 'Vladimir Zemlyakov', 'Wilfred van Rooijen',
108 'List may be incomplete - please contact me.'
110 app_copyright = '© 2005-2008 Thomas Perl and the gPodder Team'
111 app_website = 'http://www.gpodder.org/'
113 # these will be filled with pathnames in bin/gpodder
114 glade_dir = [ 'share', 'gpodder' ]
115 icon_dir = [ 'share', 'pixmaps', 'gpodder.png' ]
116 scalable_dir = [ 'share', 'icons', 'hicolor', 'scalable', 'apps', 'gpodder.svg' ]
119 class GladeWidget(SimpleGladeApp.SimpleGladeApp):
120 gpodder_main_window = None
121 finger_friendly_widgets = []
123 def __init__( self, **kwargs):
124 path = os.path.join( glade_dir, '%s.glade' % app_name)
125 root = self.__class__.__name__
126 domain = app_name
128 SimpleGladeApp.SimpleGladeApp.__init__( self, path, root, domain, **kwargs)
130 # Set widgets to finger-friendly mode if on Maemo
131 for widget_name in self.finger_friendly_widgets:
132 self.set_finger_friendly(getattr(self, widget_name))
134 if root == 'gPodder':
135 GladeWidget.gpodder_main_window = self.gPodder
136 else:
137 # If we have a child window, set it transient for our main window
138 getattr( self, root).set_transient_for( GladeWidget.gpodder_main_window)
140 if gpodder.interface == gpodder.GUI:
141 if hasattr( self, 'center_on_widget'):
142 ( x, y ) = self.gpodder_main_window.get_position()
143 a = self.center_on_widget.allocation
144 ( x, y ) = ( x + a.x, y + a.y )
145 ( w, h ) = ( a.width, a.height )
146 ( pw, ph ) = getattr( self, root).get_size()
147 getattr( self, root).move( x + w/2 - pw/2, y + h/2 - ph/2)
148 else:
149 getattr( self, root).set_position( gtk.WIN_POS_CENTER_ON_PARENT)
151 def notification(self, message, title=None):
152 util.idle_add(self.show_message, message, title)
154 def show_message( self, message, title = None):
155 if hasattr(self, 'tray_icon') and hasattr(self, 'minimized') and self.tray_icon and self.minimized:
156 if title is None:
157 title = 'gPodder'
158 self.tray_icon.send_notification(message, title)
159 return
161 if gpodder.interface == gpodder.GUI:
162 dlg = gtk.MessageDialog(GladeWidget.gpodder_main_window, gtk.DIALOG_MODAL, gtk.MESSAGE_INFO, gtk.BUTTONS_OK)
163 if title:
164 dlg.set_title(str(title))
165 dlg.set_markup('<span weight="bold" size="larger">%s</span>\n\n%s' % (title, message))
166 else:
167 dlg.set_markup('<span weight="bold" size="larger">%s</span>' % (message))
168 elif gpodder.interface == gpodder.MAEMO:
169 dlg = hildon.Note('information', (GladeWidget.gpodder_main_window, message))
171 dlg.run()
172 dlg.destroy()
174 def set_finger_friendly(self, widget):
176 If we are on Maemo, we carry out the necessary
177 operations to turn a widget into a finger-friendly
178 one, depending on which type of widget it is (i.e.
179 buttons will have more padding, TreeViews a thick
180 scrollbar, etc..)
182 if gpodder.interface == gpodder.MAEMO:
183 if isinstance(widget, gtk.Misc):
184 widget.set_padding(0, 5)
185 elif isinstance(widget, gtk.Button):
186 for child in widget.get_children():
187 if isinstance(child, gtk.Alignment):
188 child.set_padding(10, 10, 5, 5)
189 else:
190 child.set_padding(10, 10)
191 elif isinstance(widget, gtk.TreeView) or isinstance(widget, gtk.TextView):
192 parent = widget.get_parent()
193 if isinstance(parent, gtk.ScrolledWindow):
194 hildon.hildon_helper_set_thumb_scrollbar(parent, True)
195 elif isinstance(widget, gtk.MenuItem):
196 for child in widget.get_children():
197 self.set_finger_friendly(child)
198 else:
199 log('Cannot set widget finger-friendly: %s', widget, sender=self)
201 return widget
203 def show_confirmation( self, message, title = None):
204 if gpodder.interface == gpodder.GUI:
205 affirmative = gtk.RESPONSE_YES
206 dlg = gtk.MessageDialog(GladeWidget.gpodder_main_window, gtk.DIALOG_MODAL, gtk.MESSAGE_QUESTION, gtk.BUTTONS_YES_NO)
207 if title:
208 dlg.set_title(str(title))
209 dlg.set_markup('<span weight="bold" size="larger">%s</span>\n\n%s' % (title, message))
210 else:
211 dlg.set_markup('<span weight="bold" size="larger">%s</span>' % (message))
212 elif gpodder.interface == gpodder.MAEMO:
213 affirmative = gtk.RESPONSE_OK
214 dlg = hildon.Note('confirmation', (GladeWidget.gpodder_main_window, message))
216 response = dlg.run()
217 dlg.destroy()
219 return response == affirmative
221 def show_copy_dialog( self, src_filename, dst_filename = None, dst_directory = None, title = _('Select destination')):
222 if dst_filename is None:
223 dst_filename = src_filename
225 if dst_directory is None:
226 dst_directory = os.path.expanduser( '~')
228 ( base, extension ) = os.path.splitext( src_filename)
230 if not dst_filename.endswith( extension):
231 dst_filename += extension
233 if gpodder.interface == gpodder.GUI:
234 dlg = gtk.FileChooserDialog(title=title, parent=GladeWidget.gpodder_main_window, action=gtk.FILE_CHOOSER_ACTION_SAVE)
235 dlg.add_button(gtk.STOCK_CANCEL, gtk.RESPONSE_CANCEL)
236 dlg.add_button(gtk.STOCK_SAVE, gtk.RESPONSE_OK)
237 elif gpodder.interface == gpodder.MAEMO:
238 dlg = hildon.FileChooserDialog(GladeWidget.gpodder_main_window, gtk.FILE_CHOOSER_ACTION_SAVE)
240 dlg.set_do_overwrite_confirmation( True)
241 dlg.set_current_name( os.path.basename( dst_filename))
242 dlg.set_current_folder( dst_directory)
244 if dlg.run() == gtk.RESPONSE_OK:
245 dst_filename = dlg.get_filename()
246 if not dst_filename.endswith( extension):
247 dst_filename += extension
249 log( 'Copying %s => %s', src_filename, dst_filename, sender = self)
251 try:
252 shutil.copyfile( src_filename, dst_filename)
253 except:
254 log( 'Error copying file.', sender = self, traceback = True)
256 dlg.destroy()
260 class gPodder(GladeWidget):
261 finger_friendly_widgets = ['btnUpdateFeeds', 'btnCancelFeedUpdate', 'treeAvailable', 'label2', 'labelDownloads']
262 ENTER_URL_TEXT = _('Enter podcast URL...')
264 def new(self):
265 if gpodder.interface == gpodder.MAEMO:
266 # Maemo-specific changes to the UI
267 global scalable_dir
268 scalable_dir = scalable_dir.replace('.svg', '.png')
270 self.app = hildon.Program()
271 gtk.set_application_name('gPodder')
272 self.window = hildon.Window()
273 self.window.connect('delete-event', self.on_gPodder_delete_event)
274 self.window.connect('window-state-event', self.window_state_event)
275 self.window.connect('key-press-event', self.on_key_press)
277 # Give toolbar to the hildon window
278 self.toolbar.parent.remove(self.toolbar)
279 self.toolbar.set_style(gtk.TOOLBAR_ICONS)
280 self.window.add_toolbar(self.toolbar)
282 self.app.add_window(self.window)
283 self.vMain.reparent(self.window)
284 self.gPodder = self.window
286 # Reparent the main menu
287 menu = gtk.Menu()
288 for child in self.mainMenu.get_children():
289 child.reparent(menu)
290 self.itemQuit.reparent(menu)
291 self.window.set_menu(menu)
293 self.mainMenu.destroy()
294 self.window.show()
296 # do some widget hiding
297 self.toolbar.remove(self.toolTransfer)
298 self.itemTransferSelected.hide_all()
299 self.item_show_url_entry.hide_all()
300 self.item_email_subscriptions.hide_all()
302 # Feed cache update button
303 self.label120.set_text(_('Update'))
305 # get screen real estate
306 self.hboxContainer.set_border_width(0)
308 self.treeChannels.connect('size-allocate', self.on_tree_channels_resize)
310 if gpodder.interface == gpodder.MAEMO or not gl.config.show_podcast_url_entry:
311 self.hboxAddChannel.hide_all()
313 if not gl.config.show_toolbar:
314 self.toolbar.hide_all()
316 gl.config.add_observer(self.on_config_changed)
317 self.default_entry_text_color = self.entryAddChannel.get_style().text[gtk.STATE_NORMAL]
318 self.entryAddChannel.connect('focus-in-event', self.entry_add_channel_focus)
319 self.entryAddChannel.connect('focus-out-event', self.entry_add_channel_unfocus)
320 self.entry_add_channel_unfocus(self.entryAddChannel, None)
322 self.uar = None
323 self.tray_icon = None
325 self.fullscreen = False
326 self.minimized = False
327 self.gPodder.connect('window-state-event', self.window_state_event)
329 self.already_notified_new_episodes = []
330 self.show_hide_tray_icon()
331 self.no_episode_selected.set_sensitive(False)
333 self.itemShowToolbar.set_active(gl.config.show_toolbar)
334 self.itemShowDescription.set_active(gl.config.episode_list_descriptions)
335 self.item_show_url_entry.set_active(gl.config.show_podcast_url_entry)
337 gl.config.connect_gtk_window( self.gPodder)
338 gl.config.connect_gtk_paned( 'paned_position', self.channelPaned)
340 gl.config.connect_gtk_spinbutton('max_downloads', self.spinMaxDownloads)
341 gl.config.connect_gtk_togglebutton('max_downloads_enabled', self.cbMaxDownloads)
342 gl.config.connect_gtk_spinbutton('limit_rate_value', self.spinLimitDownloads)
343 gl.config.connect_gtk_togglebutton('limit_rate', self.cbLimitDownloads)
345 # Make sure we free/close the download queue when we
346 # update the "max downloads" spin button
347 changed_cb = lambda spinbutton: services.download_status_manager.update_max_downloads()
348 self.spinMaxDownloads.connect('value-changed', changed_cb)
350 self.default_title = None
351 if app_version.rfind('svn') != -1:
352 self.set_title('gPodder %s' % app_version)
353 else:
354 self.set_title(self.gPodder.get_title())
356 gtk.about_dialog_set_url_hook(lambda dlg, link, data: util.open_website(link), None)
358 # cell renderers for channel tree
359 namecolumn = gtk.TreeViewColumn( _('Podcast'))
361 iconcell = gtk.CellRendererPixbuf()
362 namecolumn.pack_start( iconcell, False)
363 namecolumn.add_attribute( iconcell, 'pixbuf', 5)
364 self.cell_channel_icon = iconcell
366 namecell = gtk.CellRendererText()
367 namecell.set_property('ellipsize', pango.ELLIPSIZE_END)
368 namecolumn.pack_start( namecell, True)
369 namecolumn.add_attribute( namecell, 'markup', 2)
371 iconcell = gtk.CellRendererPixbuf()
372 iconcell.set_property('xalign', 1.0)
373 namecolumn.pack_start( iconcell, False)
374 namecolumn.add_attribute( iconcell, 'pixbuf', 3)
375 namecolumn.add_attribute(iconcell, 'visible', 7)
376 self.cell_channel_pill = iconcell
378 self.treeChannels.append_column( namecolumn)
379 self.treeChannels.set_headers_visible(False)
381 # enable alternating colors hint
382 self.treeAvailable.set_rules_hint( True)
383 self.treeChannels.set_rules_hint( True)
385 # connect to tooltip signals
386 try:
387 self.treeChannels.set_property('has-tooltip', True)
388 self.treeChannels.connect('query-tooltip', self.treeview_channels_query_tooltip)
389 self.treeAvailable.set_property('has-tooltip', True)
390 self.treeAvailable.connect('query-tooltip', self.treeview_episodes_query_tooltip)
391 except:
392 log('I cannot set has-tooltip/query-tooltip (need at least PyGTK 2.12)', sender = self)
393 self.last_tooltip_channel = None
394 self.last_tooltip_episode = None
395 self.podcast_list_can_tooltip = True
396 self.episode_list_can_tooltip = True
398 # Add our context menu to treeAvailable
399 if gpodder.interface == gpodder.MAEMO:
400 self.treeAvailable.connect('button-release-event', self.treeview_button_pressed)
401 else:
402 self.treeAvailable.connect('button-press-event', self.treeview_button_pressed)
403 self.treeChannels.connect('button-press-event', self.treeview_channels_button_pressed)
405 iconcell = gtk.CellRendererPixbuf()
406 if gpodder.interface == gpodder.MAEMO:
407 status_column_label = ''
408 else:
409 status_column_label = _('Status')
410 iconcolumn = gtk.TreeViewColumn(status_column_label, iconcell, pixbuf=4)
412 namecell = gtk.CellRendererText()
413 namecell.set_property('ellipsize', pango.ELLIPSIZE_END)
414 namecolumn = gtk.TreeViewColumn(_("Episode"), namecell, markup=6)
415 namecolumn.set_sizing(gtk.TREE_VIEW_COLUMN_AUTOSIZE)
416 namecolumn.set_expand(True)
418 sizecell = gtk.CellRendererText()
419 sizecolumn = gtk.TreeViewColumn( _("Size"), sizecell, text=2)
421 releasecell = gtk.CellRendererText()
422 releasecolumn = gtk.TreeViewColumn( _("Released"), releasecell, text=5)
424 for itemcolumn in (iconcolumn, namecolumn, sizecolumn, releasecolumn):
425 itemcolumn.set_reorderable(True)
426 self.treeAvailable.append_column(itemcolumn)
428 if gpodder.interface == gpodder.MAEMO:
429 # Due to screen space contraints, we
430 # hide these columns here by default
431 self.column_size = sizecolumn
432 self.column_released = releasecolumn
433 self.column_released.set_visible(False)
434 self.column_size.set_visible(False)
436 # enable search in treeavailable
437 self.treeAvailable.set_search_equal_func( self.treeAvailable_search_equal)
439 # enable multiple selection support
440 self.treeAvailable.get_selection().set_mode( gtk.SELECTION_MULTIPLE)
441 self.treeDownloads.get_selection().set_mode( gtk.SELECTION_MULTIPLE)
443 # columns and renderers for "download progress" tab
444 episodecell = gtk.CellRendererText()
445 episodecell.set_property('ellipsize', pango.ELLIPSIZE_END)
446 episodecolumn = gtk.TreeViewColumn( _("Episode"), episodecell, text=0)
447 episodecolumn.set_sizing(gtk.TREE_VIEW_COLUMN_AUTOSIZE)
448 episodecolumn.set_expand(True)
450 speedcell = gtk.CellRendererText()
451 speedcolumn = gtk.TreeViewColumn( _("Speed"), speedcell, text=1)
453 progresscell = gtk.CellRendererProgress()
454 progresscolumn = gtk.TreeViewColumn( _("Progress"), progresscell, value=2)
455 progresscolumn.set_expand(True)
457 for itemcolumn in ( episodecolumn, speedcolumn, progresscolumn ):
458 self.treeDownloads.append_column( itemcolumn)
460 # After we've set up most of the window, show it :)
461 if not gpodder.interface == gpodder.MAEMO:
462 self.gPodder.show()
464 if self.tray_icon:
465 if gl.config.start_iconified:
466 self.iconify_main_window()
467 elif gl.config.minimize_to_tray:
468 self.tray_icon.set_visible(False)
470 services.download_status_manager.register( 'list-changed', self.download_status_updated)
471 services.download_status_manager.register( 'progress-changed', self.download_progress_updated)
472 services.cover_downloader.register('cover-available', self.cover_download_finished)
473 services.cover_downloader.register('cover-removed', self.cover_file_removed)
474 self.cover_cache = {}
476 self.treeDownloads.set_model( services.download_status_manager.tree_model)
478 #Add Drag and Drop Support
479 flags = gtk.DEST_DEFAULT_ALL
480 targets = [ ('text/plain', 0, 2), ('STRING', 0, 3), ('TEXT', 0, 4) ]
481 actions = gtk.gdk.ACTION_DEFAULT | gtk.gdk.ACTION_COPY
482 self.treeChannels.drag_dest_set( flags, targets, actions)
483 self.treeChannels.connect( 'drag_data_received', self.drag_data_received)
485 # Subscribed channels
486 self.active_channel = None
487 self.channels = load_channels()
489 if len(self.channels):
490 self.label2.set_text(_('Podcasts (%d)') % len(self.channels))
491 else:
492 self.label2.set_text(_('Podcasts'))
494 # load list of user applications for audio playback
495 self.user_apps_reader = UserAppsReader(['audio', 'video'])
496 Thread(target=self.read_apps).start()
498 # Clean up old, orphaned download files
499 gl.clean_up_downloads( delete_partial = True)
501 # Set the "Device" menu item for the first time
502 self.update_item_device()
504 # Now, update the feed cache, when everything's in place
505 self.feed_cache_update_cancelled = False
506 self.update_feed_cache(force_update=gl.config.update_on_startup)
508 # Start the auto-update procedure
509 self.auto_update_procedure(first_run=True)
511 # Delete old episodes if the user wishes to
512 if gl.config.auto_remove_old_episodes:
513 old_episodes = self.get_old_episodes()
514 if len(old_episodes) > 0:
515 self.delete_episode_list(old_episodes, confirm=False)
516 self.updateComboBox()
518 # First-time users should be asked if they want to see the OPML
519 if len(self.channels) == 0:
520 util.idle_add(self.on_itemUpdate_activate, None)
522 def on_tree_channels_resize(self, widget, allocation):
523 if not gl.config.podcast_sidebar_save_space:
524 return
526 window_allocation = self.gPodder.get_allocation()
527 percentage = 100. * float(allocation.width) / float(window_allocation.width)
528 if hasattr(self, 'cell_channel_icon'):
529 self.cell_channel_icon.set_property('visible', bool(percentage > 22.))
530 if hasattr(self, 'cell_channel_pill'):
531 self.cell_channel_pill.set_property('visible', bool(percentage > 25.))
533 def entry_add_channel_focus(self, widget, event):
534 widget.modify_text(gtk.STATE_NORMAL, self.default_entry_text_color)
535 if widget.get_text() == self.ENTER_URL_TEXT:
536 widget.set_text('')
538 def entry_add_channel_unfocus(self, widget, event):
539 if widget.get_text() == '':
540 widget.set_text(self.ENTER_URL_TEXT)
541 widget.modify_text(gtk.STATE_NORMAL, gtk.gdk.color_parse('#aaaaaa'))
543 def on_config_changed(self, name, old_value, new_value):
544 if name == 'show_toolbar':
545 if new_value:
546 self.toolbar.show_all()
547 else:
548 self.toolbar.hide_all()
549 elif name == 'episode_list_descriptions':
550 for channel in self.channels:
551 channel.force_update_tree_model()
552 self.updateTreeView()
553 elif name == 'show_podcast_url_entry' and gpodder.interface != gpodder.MAEMO:
554 if new_value:
555 self.hboxAddChannel.show_all()
556 else:
557 self.hboxAddChannel.hide_all()
559 def read_apps(self):
560 time.sleep(3) # give other parts of gpodder a chance to start up
561 self.user_apps_reader.read()
562 util.idle_add(self.user_apps_reader.get_applications_as_model, 'audio', False)
563 util.idle_add(self.user_apps_reader.get_applications_as_model, 'video', False)
565 def treeview_episodes_query_tooltip(self, treeview, x, y, keyboard_tooltip, tooltip):
566 # With get_bin_window, we get the window that contains the rows without
567 # the header. The Y coordinate of this window will be the height of the
568 # treeview header. This is the amount we have to subtract from the
569 # event's Y coordinate to get the coordinate to pass to get_path_at_pos
570 (x_bin, y_bin) = treeview.get_bin_window().get_position()
571 y -= x_bin
572 y -= y_bin
573 (path, column, rx, ry) = treeview.get_path_at_pos( x, y) or (None,)*4
575 if not self.episode_list_can_tooltip:
576 self.last_tooltip_episode = None
577 return False
579 if path is not None:
580 model = treeview.get_model()
581 iter = model.get_iter(path)
582 url = model.get_value(iter, 0)
583 description = model.get_value(iter, 7)
584 if self.last_tooltip_episode is not None and self.last_tooltip_episode != url:
585 self.last_tooltip_episode = None
586 return False
587 self.last_tooltip_episode = url
589 tooltip.set_text(description)
590 return True
592 self.last_tooltip_episode = None
593 return False
595 def podcast_list_allow_tooltips(self):
596 self.podcast_list_can_tooltip = True
598 def episode_list_allow_tooltips(self):
599 self.episode_list_can_tooltip = True
601 def treeview_channels_query_tooltip(self, treeview, x, y, keyboard_tooltip, tooltip):
602 (path, column, rx, ry) = treeview.get_path_at_pos( x, y) or (None,)*4
604 if not self.podcast_list_can_tooltip:
605 self.last_tooltip_channel = None
606 return False
608 if path is not None:
609 model = treeview.get_model()
610 iter = model.get_iter(path)
611 url = model.get_value(iter, 0)
612 for channel in self.channels:
613 if channel.url == url:
614 if self.last_tooltip_channel is not None and self.last_tooltip_channel != channel:
615 self.last_tooltip_channel = None
616 return False
617 self.last_tooltip_channel = channel
618 channel.request_save_dir_size()
619 diskspace_str = gl.format_filesize(channel.save_dir_size, 0)
620 error_str = model.get_value(iter, 6)
621 if error_str:
622 error_str = _('Feedparser error: %s') % saxutils.escape(error_str.strip())
623 error_str = '<span foreground="#ff0000">%s</span>' % error_str
624 table = gtk.Table(rows=3, columns=3)
625 table.set_row_spacings(5)
626 table.set_col_spacings(5)
627 table.set_border_width(5)
629 heading = gtk.Label()
630 heading.set_alignment(0, 1)
631 heading.set_markup('<b><big>%s</big></b>\n<small>%s</small>' % (saxutils.escape(channel.title), saxutils.escape(channel.url)))
632 table.attach(heading, 0, 1, 0, 1)
633 size_info = gtk.Label()
634 size_info.set_alignment(1, 1)
635 size_info.set_justify(gtk.JUSTIFY_RIGHT)
636 size_info.set_markup('<b>%s</b>\n<small>%s</small>' % (diskspace_str, _('disk usage')))
637 table.attach(size_info, 2, 3, 0, 1)
639 table.attach(gtk.HSeparator(), 0, 3, 1, 2)
641 if len(channel.description) < 500:
642 description = channel.description
643 else:
644 pos = channel.description.find('\n\n')
645 if pos == -1 or pos > 500:
646 description = channel.description[:498]+'[...]'
647 else:
648 description = channel.description[:pos]
650 description = gtk.Label(description)
651 if error_str:
652 description.set_markup(error_str)
653 description.set_alignment(0, 0)
654 description.set_line_wrap(True)
655 table.attach(description, 0, 3, 2, 3)
657 table.show_all()
658 tooltip.set_custom(table)
660 return True
662 self.last_tooltip_channel = None
663 return False
665 def update_m3u_playlist_clicked(self, widget):
666 self.active_channel.update_m3u_playlist()
667 self.show_message(_('Updated M3U playlist in download folder.'), _('Updated playlist'))
669 def treeview_channels_button_pressed( self, treeview, event):
670 global WEB_BROWSER_ICON
672 if event.button == 3:
673 ( x, y ) = ( int(event.x), int(event.y) )
674 ( path, column, rx, ry ) = treeview.get_path_at_pos( x, y) or (None,)*4
676 paths = []
678 # Did the user right-click into a selection?
679 selection = treeview.get_selection()
680 if selection.count_selected_rows() and path:
681 ( model, paths ) = selection.get_selected_rows()
682 if path not in paths:
683 # We have right-clicked, but not into the
684 # selection, assume we don't want to operate
685 # on the selection
686 paths = []
688 # No selection or right click not in selection:
689 # Select the single item where we clicked
690 if not len( paths) and path:
691 treeview.grab_focus()
692 treeview.set_cursor( path, column, 0)
694 ( model, paths ) = ( treeview.get_model(), [ path ] )
696 # We did not find a selection, and the user didn't
697 # click on an item to select -- don't show the menu
698 if not len( paths):
699 return True
701 menu = gtk.Menu()
703 item = gtk.ImageMenuItem( _('Open download folder'))
704 item.set_image( gtk.image_new_from_icon_name( 'folder-open', gtk.ICON_SIZE_MENU))
705 item.connect('activate', lambda x: util.gui_open(self.active_channel.save_dir))
706 menu.append( item)
708 if gl.config.create_m3u_playlists:
709 item = gtk.ImageMenuItem(_('Update M3U playlist'))
710 item.set_image(gtk.image_new_from_stock(gtk.STOCK_REFRESH, gtk.ICON_SIZE_MENU))
711 item.connect('activate', self.update_m3u_playlist_clicked)
712 menu.append(item)
714 if self.active_channel.link:
715 item = gtk.ImageMenuItem(_('Visit website'))
716 item.set_image(gtk.image_new_from_icon_name(WEB_BROWSER_ICON, gtk.ICON_SIZE_MENU))
717 item.connect('activate', lambda w: util.open_website(self.active_channel.link))
718 menu.append(item)
720 menu.append( gtk.SeparatorMenuItem())
722 item = gtk.ImageMenuItem(gtk.STOCK_EDIT)
723 item.connect( 'activate', self.on_itemEditChannel_activate)
724 menu.append( item)
726 item = gtk.ImageMenuItem(gtk.STOCK_DELETE)
727 item.connect( 'activate', self.on_itemRemoveChannel_activate)
728 menu.append( item)
730 menu.show_all()
731 # Disable tooltips while we are showing the menu, so
732 # the tooltip will not appear over the menu
733 self.podcast_list_can_tooltip = False
734 menu.connect('deactivate', lambda menushell: self.podcast_list_allow_tooltips())
735 menu.popup( None, None, None, event.button, event.time)
737 return True
739 def on_itemClose_activate(self, widget):
740 if self.tray_icon is not None:
741 if gpodder.interface == gpodder.MAEMO:
742 self.gPodder.set_property('visible', False)
743 else:
744 self.iconify_main_window()
745 else:
746 self.on_gPodder_delete_event(widget)
748 def cover_file_removed(self, channel_url):
750 The Cover Downloader calls this when a previously-
751 available cover has been removed from the disk. We
752 have to update our cache to reflect this change.
754 (COLUMN_URL, COLUMN_PIXBUF) = (0, 5)
755 for row in self.treeChannels.get_model():
756 if row[COLUMN_URL] == channel_url:
757 row[COLUMN_PIXBUF] = None
758 key = (channel_url, gl.config.podcast_list_icon_size, \
759 gl.config.podcast_list_icon_size)
760 if key in self.cover_cache:
761 del self.cover_cache[key]
764 def cover_download_finished(self, channel_url, pixbuf):
766 The Cover Downloader calls this when it has finished
767 downloading (or registering, if already downloaded)
768 a new channel cover, which is ready for displaying.
770 if pixbuf is not None:
771 (COLUMN_URL, COLUMN_PIXBUF) = (0, 5)
772 for row in self.treeChannels.get_model():
773 if row[COLUMN_URL] == channel_url and row[COLUMN_PIXBUF] is None:
774 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)
775 row[COLUMN_PIXBUF] = new_pixbuf or pixbuf
777 def save_episode_as_file( self, url, *args):
778 episode = self.active_channel.find_episode( url)
780 self.show_copy_dialog( src_filename = episode.local_filename(), dst_filename = episode.sync_filename())
782 def copy_episode_bluetooth(self, url, *args):
783 episode = self.active_channel.find_episode(url)
784 filename = episode.local_filename()
786 if gl.config.bluetooth_ask_always:
787 device = None
788 else:
789 device = gl.config.bluetooth_device_address
791 destfile = os.path.join(gl.tempdir, episode.sync_filename())
792 (base, ext) = os.path.splitext(filename)
793 if not destfile.endswith(ext):
794 destfile += ext
796 if gl.config.bluetooth_use_converter:
797 title = _('Converting file')
798 message = _('Please wait while gPodder converts your media file for bluetooth file transfer.')
799 dlg = gtk.MessageDialog(self.gPodder, gtk.DIALOG_MODAL, gtk.MESSAGE_INFO, gtk.BUTTONS_NONE)
800 dlg.set_title(title)
801 dlg.set_markup('<span weight="bold" size="larger">%s</span>\n\n%s'%(title, message))
802 dlg.show_all()
803 else:
804 dlg = None
806 def convert_and_send_thread(filename, destfile, device, dialog, notify):
807 if gl.config.bluetooth_use_converter:
808 p = subprocess.Popen([gl.config.bluetooth_converter, filename, destfile], stdout=sys.stdout, stderr=sys.stderr)
809 result = p.wait()
810 if dialog is not None:
811 dialog.destroy()
812 else:
813 try:
814 shutil.copyfile(filename, destfile)
815 result = 0
816 except:
817 log('Cannot copy "%s" to "%s".', filename, destfile, sender=self)
818 result = 1
820 if result == 0 or not os.path.exists(destfile):
821 util.bluetooth_send_file(destfile, device)
822 else:
823 notify(_('Error converting file.'), _('Bluetooth file transfer'))
824 util.delete_file(destfile)
826 Thread(target=convert_and_send_thread, args=[filename, destfile, device, dlg, self.notification]).start()
828 def treeview_button_pressed( self, treeview, event):
829 global WEB_BROWSER_ICON
831 # Use right-click for the Desktop version and left-click for Maemo
832 if (event.button == 1 and gpodder.interface == gpodder.MAEMO) or \
833 (event.button == 3 and gpodder.interface == gpodder.GUI):
834 ( x, y ) = ( int(event.x), int(event.y) )
835 ( path, column, rx, ry ) = treeview.get_path_at_pos( x, y) or (None,)*4
837 paths = []
839 # Did the user right-click into a selection?
840 selection = self.treeAvailable.get_selection()
841 if selection.count_selected_rows() and path:
842 ( model, paths ) = selection.get_selected_rows()
843 if path not in paths:
844 # We have right-clicked, but not into the
845 # selection, assume we don't want to operate
846 # on the selection
847 paths = []
849 # No selection or right click not in selection:
850 # Select the single item where we clicked
851 if not len( paths) and path:
852 treeview.grab_focus()
853 treeview.set_cursor( path, column, 0)
855 ( model, paths ) = ( treeview.get_model(), [ path ] )
857 # We did not find a selection, and the user didn't
858 # click on an item to select -- don't show the menu
859 if not len( paths):
860 return True
862 first_url = model.get_value( model.get_iter( paths[0]), 0)
863 episode = db.load_episode(first_url)
865 menu = gtk.Menu()
867 ( can_play, can_download, can_transfer, can_cancel, can_delete ) = self.play_or_download()
869 if can_play:
870 item = gtk.ImageMenuItem(gtk.STOCK_MEDIA_PLAY)
871 item.connect( 'activate', lambda w: self.on_treeAvailable_row_activated( self.toolPlay))
872 menu.append(self.set_finger_friendly(item))
874 if not episode['is_locked'] and can_delete:
875 item = gtk.ImageMenuItem(gtk.STOCK_DELETE)
876 item.connect('activate', self.on_btnDownloadedDelete_clicked)
877 menu.append(self.set_finger_friendly(item))
879 if can_cancel:
880 item = gtk.ImageMenuItem( _('Cancel download'))
881 item.set_image( gtk.image_new_from_stock( gtk.STOCK_STOP, gtk.ICON_SIZE_MENU))
882 item.connect( 'activate', lambda w: self.on_treeDownloads_row_activated( self.toolCancel))
883 menu.append(self.set_finger_friendly(item))
885 if can_download:
886 item = gtk.ImageMenuItem(_('Download'))
887 item.set_image( gtk.image_new_from_stock( gtk.STOCK_GO_DOWN, gtk.ICON_SIZE_MENU))
888 item.connect( 'activate', lambda w: self.on_treeAvailable_row_activated( self.toolDownload))
889 menu.append(self.set_finger_friendly(item))
891 if episode['state'] == db.STATE_NORMAL and not episode['is_played']: # can_download:
892 item = gtk.ImageMenuItem(_('Do not download'))
893 item.set_image(gtk.image_new_from_stock(gtk.STOCK_DELETE, gtk.ICON_SIZE_MENU))
894 item.connect('activate', lambda w: self.mark_selected_episodes_old())
895 menu.append(self.set_finger_friendly(item))
896 elif episode['state'] != db.STATE_NORMAL and can_download:
897 item = gtk.ImageMenuItem(_('Mark as new'))
898 item.set_image(gtk.image_new_from_stock(gtk.STOCK_ABOUT, gtk.ICON_SIZE_MENU))
899 item.connect('activate', lambda w: self.mark_selected_episodes_new())
900 menu.append(self.set_finger_friendly(item))
902 if can_play:
903 menu.append( gtk.SeparatorMenuItem())
904 item = gtk.ImageMenuItem(_('Save to disk'))
905 item.set_image(gtk.image_new_from_stock(gtk.STOCK_SAVE_AS, gtk.ICON_SIZE_MENU))
906 item.connect( 'activate', lambda w: self.save_episode_as_file( episode_url))
907 menu.append(self.set_finger_friendly(item))
908 if gl.config.bluetooth_enabled:
909 item = gtk.ImageMenuItem(_('Send via bluetooth'))
910 item.set_image(gtk.image_new_from_icon_name('bluetooth', gtk.ICON_SIZE_MENU))
911 item.connect('activate', lambda w: self.copy_episode_bluetooth(episode_url))
912 menu.append(self.set_finger_friendly(item))
913 if can_transfer:
914 item = gtk.ImageMenuItem(_('Transfer to %s') % gl.get_device_name())
915 item.set_image(gtk.image_new_from_icon_name('multimedia-player', gtk.ICON_SIZE_MENU))
916 item.connect('activate', lambda w: self.on_treeAvailable_row_activated(self.toolTransfer))
917 menu.append(self.set_finger_friendly(item))
919 if can_play:
920 menu.append( gtk.SeparatorMenuItem())
921 is_played = episode['is_played']
922 if is_played:
923 item = gtk.ImageMenuItem(_('Mark as unplayed'))
924 item.set_image( gtk.image_new_from_stock( gtk.STOCK_CANCEL, gtk.ICON_SIZE_MENU))
925 item.connect( 'activate', lambda w: self.on_item_toggle_played_activate( w, False, False))
926 menu.append(self.set_finger_friendly(item))
927 else:
928 item = gtk.ImageMenuItem(_('Mark as played'))
929 item.set_image( gtk.image_new_from_stock( gtk.STOCK_APPLY, gtk.ICON_SIZE_MENU))
930 item.connect( 'activate', lambda w: self.on_item_toggle_played_activate( w, False, True))
931 menu.append(self.set_finger_friendly(item))
933 is_locked = episode['is_locked']
934 if is_locked:
935 item = gtk.ImageMenuItem(_('Allow deletion'))
936 item.set_image(gtk.image_new_from_stock(gtk.STOCK_DIALOG_AUTHENTICATION, gtk.ICON_SIZE_MENU))
937 item.connect('activate', self.on_item_toggle_lock_activate)
938 menu.append(self.set_finger_friendly(item))
939 else:
940 item = gtk.ImageMenuItem(_('Prohibit deletion'))
941 item.set_image(gtk.image_new_from_stock(gtk.STOCK_DIALOG_AUTHENTICATION, gtk.ICON_SIZE_MENU))
942 item.connect('activate', self.on_item_toggle_lock_activate)
943 menu.append(self.set_finger_friendly(item))
945 if len(paths) == 1:
946 menu.append(gtk.SeparatorMenuItem())
947 # Single item, add episode information menu item
948 episode_url = model.get_value( model.get_iter( paths[0]), 0)
949 item = gtk.ImageMenuItem(_('Episode details'))
950 item.set_image( gtk.image_new_from_stock( gtk.STOCK_INFO, gtk.ICON_SIZE_MENU))
951 item.connect( 'activate', lambda w: self.on_treeAvailable_row_activated( self.treeAvailable))
952 menu.append(self.set_finger_friendly(item))
953 episode = self.active_channel.find_episode(episode_url)
954 # If we have it, also add episode website link
955 if episode and episode.link and episode.link != episode.url:
956 item = gtk.ImageMenuItem(_('Visit website'))
957 item.set_image(gtk.image_new_from_icon_name(WEB_BROWSER_ICON, gtk.ICON_SIZE_MENU))
958 item.connect('activate', lambda w: util.open_website(episode.link))
959 menu.append(self.set_finger_friendly(item))
961 if gpodder.interface == gpodder.MAEMO:
962 # Because we open the popup on left-click for Maemo,
963 # we also include a non-action to close the menu
964 menu.append(gtk.SeparatorMenuItem())
965 item = gtk.ImageMenuItem(_('Close this menu'))
966 item.set_image(gtk.image_new_from_stock(gtk.STOCK_CLOSE, gtk.ICON_SIZE_MENU))
967 menu.append(self.set_finger_friendly(item))
969 menu.show_all()
970 # Disable tooltips while we are showing the menu, so
971 # the tooltip will not appear over the menu
972 self.episode_list_can_tooltip = False
973 menu.connect('deactivate', lambda menushell: self.episode_list_allow_tooltips())
974 menu.popup( None, None, None, event.button, event.time)
976 return True
978 def set_title(self, new_title):
979 self.default_title = new_title
980 self.gPodder.set_title(new_title)
982 def download_progress_updated( self, count, percentage):
983 title = [ self.default_title ]
985 if count == 1:
986 title.append( _('downloading one file'))
987 elif count > 1:
988 title.append( _('downloading %d files') % count)
990 if len(title) == 2:
991 title[1] = ''.join( [ title[1], ' (%d%%)' % ( percentage, ) ])
993 self.gPodder.set_title( ' - '.join( title))
995 # Have all the downloads completed?
996 # If so execute user command if defined, else do nothing
997 if count == 0:
998 if len(gl.config.cmd_all_downloads_complete) > 0:
999 Thread(target=gl.ext_command_thread, args=(self.notification,gl.config.cmd_all_downloads_complete)).start()
1001 def playback_episode( self, current_channel, current_podcast):
1002 (success, application) = gl.playback_episode(current_channel, current_podcast)
1003 if not success:
1004 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), ))
1005 self.download_status_updated()
1007 def treeAvailable_search_equal( self, model, column, key, iter, data = None):
1008 if model is None:
1009 return True
1011 key = key.lower()
1013 # columns, as defined in libpodcasts' get model method
1014 # 1 = episode title, 7 = description
1015 columns = (1, 7)
1017 for column in columns:
1018 value = model.get_value( iter, column).lower()
1019 if value.find( key) != -1:
1020 return False
1022 return True
1024 def change_menu_item(self, menuitem, icon=None, label=None):
1025 (label_widget, icon_widget) = menuitem.get_children()
1026 if icon is not None:
1027 icon_widget.set_from_icon_name(icon, gtk.ICON_SIZE_MENU)
1028 if label is not None:
1029 label_widget.set_text(label)
1031 def play_or_download(self):
1032 if self.wNotebook.get_current_page() > 0:
1033 return
1035 ( can_play, can_download, can_transfer, can_cancel, can_delete ) = (False,)*5
1036 ( is_played, is_locked ) = (False,)*2
1038 selection = self.treeAvailable.get_selection()
1039 if selection.count_selected_rows() > 0:
1040 (model, paths) = selection.get_selected_rows()
1042 for path in paths:
1043 url = model.get_value( model.get_iter( path), 0)
1044 local_filename = model.get_value( model.get_iter( path), 8)
1046 episode = podcastItem.load(url, self.active_channel)
1048 if episode.was_downloaded(and_exists=True):
1049 can_play = True
1050 can_delete = True
1051 is_played = episode.is_played
1052 is_locked = episode.is_locked
1053 else:
1054 if services.download_status_manager.is_download_in_progress(url):
1055 can_cancel = True
1056 else:
1057 can_download = True
1059 if self.active_channel.find_episode(url).file_type() == 'torrent':
1060 can_download = can_download or gl.config.use_gnome_bittorrent
1062 can_download = can_download and not can_cancel
1063 can_play = can_play and not can_cancel and not can_download
1064 can_transfer = can_play and gl.config.device_type != 'none'
1066 self.toolPlay.set_sensitive( can_play)
1067 self.toolDownload.set_sensitive( can_download)
1068 self.toolTransfer.set_sensitive( can_transfer)
1069 self.toolCancel.set_sensitive( can_cancel)
1071 if can_cancel:
1072 self.item_cancel_download.show_all()
1073 else:
1074 self.item_cancel_download.hide_all()
1075 if can_download:
1076 self.itemDownloadSelected.show_all()
1077 else:
1078 self.itemDownloadSelected.hide_all()
1079 if can_play:
1080 self.itemPlaySelected.show_all()
1081 self.itemDeleteSelected.show_all()
1082 self.item_toggle_played.show_all()
1083 self.item_toggle_lock.show_all()
1084 self.separator9.show_all()
1085 if is_played:
1086 self.change_menu_item(self.item_toggle_played, gtk.STOCK_CANCEL, _('Mark as unplayed'))
1087 else:
1088 self.change_menu_item(self.item_toggle_played, gtk.STOCK_APPLY, _('Mark as played'))
1089 if is_locked:
1090 self.change_menu_item(self.item_toggle_lock, gtk.STOCK_DIALOG_AUTHENTICATION, _('Allow deletion'))
1091 else:
1092 self.change_menu_item(self.item_toggle_lock, gtk.STOCK_DIALOG_AUTHENTICATION, _('Prohibit deletion'))
1093 else:
1094 self.itemPlaySelected.hide_all()
1095 self.itemDeleteSelected.hide_all()
1096 self.item_toggle_played.hide_all()
1097 self.item_toggle_lock.hide_all()
1098 self.separator9.hide_all()
1099 if can_play or can_download or can_cancel:
1100 self.item_episode_details.show_all()
1101 self.separator16.show_all()
1102 self.no_episode_selected.hide_all()
1103 else:
1104 self.item_episode_details.hide_all()
1105 self.separator16.hide_all()
1106 self.no_episode_selected.show_all()
1108 return ( can_play, can_download, can_transfer, can_cancel, can_delete )
1110 def download_status_updated( self):
1111 count = services.download_status_manager.count()
1112 if count:
1113 self.labelDownloads.set_text( _('Downloads (%d)') % count)
1114 else:
1115 self.labelDownloads.set_text( _('Downloads'))
1117 for channel in self.channels:
1118 channel.update_model()
1120 self.updateComboBox()
1122 def on_cbMaxDownloads_toggled(self, widget, *args):
1123 self.spinMaxDownloads.set_sensitive(self.cbMaxDownloads.get_active())
1125 def on_cbLimitDownloads_toggled(self, widget, *args):
1126 self.spinLimitDownloads.set_sensitive(self.cbLimitDownloads.get_active())
1128 def updateComboBox(self, selected_url=None):
1129 (model, iter) = self.treeChannels.get_selection().get_selected()
1131 if model and iter and selected_url is None:
1132 # Get the URL of the currently-selected podcast
1133 selected_url = model.get_value(iter, 0)
1135 rect = self.treeChannels.get_visible_rect()
1136 self.treeChannels.set_model(channels_to_model(self.channels, self.cover_cache, gl.config.podcast_list_icon_size, gl.config.podcast_list_icon_size))
1137 util.idle_add(self.treeChannels.scroll_to_point, rect.x, rect.y)
1139 for channel in self.channels:
1140 services.cover_downloader.request_cover(channel)
1142 try:
1143 selected_path = (0,)
1144 # Find the previously-selected URL in the new
1145 # model if we have an URL (else select first)
1146 if selected_url is not None:
1147 model = self.treeChannels.get_model()
1148 pos = model.get_iter_first()
1149 while pos is not None:
1150 url = model.get_value(pos, 0)
1151 if url == selected_url:
1152 selected_path = model.get_path(pos)
1153 break
1154 pos = model.iter_next(pos)
1156 self.treeChannels.get_selection().select_path(selected_path)
1157 except:
1158 log( 'Cannot set selection on treeChannels', sender = self)
1159 self.on_treeChannels_cursor_changed( self.treeChannels)
1161 def updateTreeView( self):
1162 if self.channels and self.active_channel is not None:
1163 self.treeAvailable.set_model(self.active_channel.tree_model)
1164 self.treeAvailable.columns_autosize()
1165 self.play_or_download()
1166 else:
1167 if self.treeAvailable.get_model():
1168 self.treeAvailable.get_model().clear()
1170 def drag_data_received(self, widget, context, x, y, sel, ttype, time):
1171 (path, column, rx, ry) = self.treeChannels.get_path_at_pos( x, y) or (None,)*4
1173 dnd_channel = None
1174 if path is not None:
1175 model = self.treeChannels.get_model()
1176 iter = model.get_iter(path)
1177 url = model.get_value(iter, 0)
1178 for channel in self.channels:
1179 if channel.url == url:
1180 dnd_channel = channel
1181 break
1183 result = sel.data
1184 rl = result.strip().lower()
1185 if (rl.endswith('.jpg') or rl.endswith('.png') or rl.endswith('.gif') or rl.endswith('.svg')) and dnd_channel is not None:
1186 services.cover_downloader.replace_cover(dnd_channel, result)
1187 else:
1188 self.add_new_channel(result)
1190 def add_new_channel(self, result=None, ask_download_new=True, quiet=False):
1191 result = util.normalize_feed_url( result)
1193 if result:
1194 for old_channel in self.channels:
1195 if old_channel.url == result:
1196 log( 'Channel already exists: %s', result)
1197 # Select the existing channel in combo box
1198 for i in range( len( self.channels)):
1199 if self.channels[i] == old_channel:
1200 self.treeChannels.get_selection().select_path( (i,))
1201 self.on_treeChannels_cursor_changed(self.treeChannels)
1202 break
1203 self.show_message( _('You have already subscribed to this podcast: %s') % ( saxutils.escape( old_channel.title), ), _('Already added'))
1204 return
1205 log( 'Adding new channel: %s', result)
1206 try:
1207 channel = podcastChannel.load(url=result, create=True)
1208 except Exception, e:
1209 log('Error in podcastChannel.load(%s): %s', result, e, backtrace=True, sender=self)
1210 channel = None
1212 if channel is not None:
1213 self.channels.append( channel)
1214 save_channels( self.channels)
1215 if not quiet:
1216 # download changed channels and select the new episode in the UI afterwards
1217 self.update_feed_cache(force_update=False, select_url_afterwards=channel.url)
1219 (username, password) = util.username_password_from_url( result)
1220 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')):
1221 channel.username = username
1222 channel.password = password
1223 log('Saving authentication data for episode downloads..', sender = self)
1224 channel.save()
1226 if ask_download_new:
1227 new_episodes = channel.get_new_episodes()
1228 if len(new_episodes):
1229 self.new_episodes_show(new_episodes)
1230 else:
1231 # Ok, the URL is not a channel, or there is some other
1232 # error - let's see if it's a web page or OPML file...
1233 try:
1234 data = urllib2.urlopen(result).read().lower()
1235 if '</opml>' in data:
1236 # This looks like an OPML feed
1237 self.on_item_import_from_file_activate(None, result)
1238 return
1239 elif '</html>' in data:
1240 # This looks like a web page
1241 title = _('The URL is a website')
1242 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.)')
1243 if self.show_confirmation(message, title):
1244 util.open_website(result)
1245 return
1246 except Exception, e:
1247 log('Error trying to handle the URL as OPML or web page: %s', e, sender=self)
1249 title = _('Error adding podcast')
1250 message = _('The podcast could not be added. Please check the spelling of the URL or try again later.')
1251 self.show_message( message, title)
1252 else:
1253 if result:
1254 title = _('URL scheme not supported')
1255 message = _('gPodder currently only supports URLs starting with <b>http://</b>, <b>feed://</b> or <b>ftp://</b>.')
1256 self.show_message( message, title)
1257 else:
1258 self.show_message(_('There has been an error adding this podcast. Please see the log output for more information.'), _('Error adding podcast'))
1260 def update_feed_cache_callback(self, progressbar, position, count, force_update):
1261 if position < len(self.channels):
1262 title = self.channels[position].title
1263 if force_update:
1264 progression = _('Updating %s (%d/%d)')%(title, position+1, count)
1265 else:
1266 progression = _('Loading %s (%d/%d)')%(title, position+1, count)
1267 progressbar.set_text(progression)
1268 if self.tray_icon:
1269 self.tray_icon.set_status(self.tray_icon.STATUS_UPDATING_FEED_CACHE, progression)
1271 if count > 0:
1272 progressbar.set_fraction(float(position)/float(count))
1274 def update_feed_cache_finish_callback(self, force_update=False, notify_no_new_episodes=False, select_url_afterwards=None):
1275 self.hboxUpdateFeeds.hide_all()
1276 self.btnUpdateFeeds.show_all()
1278 # If we want to select a specific podcast (via its URL)
1279 # after the update, we give it to updateComboBox here to
1280 # select exactly this podcast after updating the view
1281 self.updateComboBox(selected_url=select_url_afterwards)
1283 if self.tray_icon:
1284 self.tray_icon.set_status(None)
1285 if self.minimized and force_update:
1286 new_episodes = []
1287 # look for new episodes to notify
1288 for channel in self.channels:
1289 for episode in channel.get_new_episodes():
1290 if not episode in self.already_notified_new_episodes:
1291 new_episodes.append(episode)
1292 self.already_notified_new_episodes.append(episode)
1293 # notify new episodes
1295 if len(new_episodes) == 0:
1296 if notify_no_new_episodes and self.tray_icon is not None:
1297 msg = _('No new episodes available for download')
1298 self.tray_icon.send_notification(msg)
1299 return
1300 elif len(new_episodes) == 1:
1301 title = _('gPodder has found %s') % (_('one new episode:'),)
1302 else:
1303 title = _('gPodder has found %s') % (_('%i new episodes:') % len(new_episodes))
1304 message = self.tray_icon.format_episode_list(new_episodes)
1306 #auto download new episodes
1307 if gl.config.auto_download_when_minimized:
1308 message += '\n<i>(%s...)</i>' % _('downloading')
1309 self.download_episode_list(new_episodes)
1310 self.tray_icon.send_notification(message, title)
1311 return
1313 # open the episodes selection dialog
1314 if force_update:
1315 self.on_itemDownloadAllNew_activate( self.gPodder)
1317 def update_feed_cache_proc( self, force_update, callback_proc = None, callback_error = None, finish_proc = None):
1318 if not force_update:
1319 self.channels = load_channels()
1320 else:
1321 is_cancelled_cb = lambda: self.feed_cache_update_cancelled
1322 self.channels = update_channels(callback_proc=callback_proc, callback_error=callback_error, is_cancelled_cb=is_cancelled_cb)
1324 self.pbFeedUpdate.set_text(_('Building list...'))
1325 if finish_proc:
1326 finish_proc()
1328 def on_btnCancelFeedUpdate_clicked(self, widget):
1329 self.pbFeedUpdate.set_text(_('Cancelling...'))
1330 self.feed_cache_update_cancelled = True
1332 def update_feed_cache(self, force_update=True, notify_no_new_episodes=False, select_url_afterwards=None):
1333 if self.tray_icon:
1334 self.tray_icon.set_status(self.tray_icon.STATUS_UPDATING_FEED_CACHE)
1336 # let's get down to business..
1337 callback_proc = lambda pos, count: util.idle_add(self.update_feed_cache_callback, self.pbFeedUpdate, pos, count, force_update)
1338 finish_proc = lambda: util.idle_add(self.update_feed_cache_finish_callback, force_update, notify_no_new_episodes, select_url_afterwards)
1340 self.feed_cache_update_cancelled = False
1341 self.btnUpdateFeeds.hide_all()
1342 self.hboxUpdateFeeds.show_all()
1344 args = (force_update, callback_proc, self.notification, finish_proc)
1346 thread = Thread( target = self.update_feed_cache_proc, args = args)
1347 thread.start()
1349 def download_podcast_by_url( self, url, want_message_dialog = True, widget = None):
1350 if self.active_channel is None:
1351 return
1353 current_channel = self.active_channel
1354 current_podcast = current_channel.find_episode( url)
1355 filename = current_podcast.local_filename()
1357 if widget:
1358 if (widget.get_name() == 'itemPlaySelected' or widget.get_name() == 'toolPlay') and os.path.exists( filename):
1359 # addDownloadedItem just to make sure the episode is marked correctly in localdb
1360 current_channel.addDownloadedItem( current_podcast)
1361 # open the file now
1362 if current_podcast.file_type() != 'torrent':
1363 self.playback_episode( current_channel, current_podcast)
1364 return
1366 if widget.get_name() == 'treeAvailable' or widget.get_name() == 'item_episode_details':
1367 play_callback = lambda: self.playback_episode( current_channel, current_podcast)
1368 download_callback = lambda: self.download_podcast_by_url( url, want_message_dialog, None)
1369 gpe = gPodderEpisode( episode = current_podcast, channel = current_channel, download_callback = download_callback, play_callback = play_callback)
1370 return
1372 if not os.path.exists( filename) and not services.download_status_manager.is_download_in_progress( current_podcast.url):
1373 download.DownloadThread( current_channel, current_podcast, self.notification).start()
1374 else:
1375 if want_message_dialog and os.path.exists( filename) and not current_podcast.file_type() == 'torrent':
1376 title = _('Episode already downloaded')
1377 message = _('You have already downloaded this episode. Click on the episode to play it.')
1378 self.show_message( message, title)
1379 elif want_message_dialog and not current_podcast.file_type() == 'torrent':
1380 title = _('Download in progress')
1381 message = _('You are currently downloading this episode. Please check the download status tab to check when the download is finished.')
1382 self.show_message( message, title)
1384 if os.path.exists( filename):
1385 log( 'Episode has already been downloaded.')
1386 current_channel.addDownloadedItem( current_podcast)
1387 self.updateComboBox()
1389 def on_gPodder_delete_event(self, widget, *args):
1390 """Called when the GUI wants to close the window
1391 Displays a confirmation dialog (and closes/hides gPodder)
1394 downloading = services.download_status_manager.has_items()
1396 # Only iconify if we are using the window's "X" button,
1397 # but not when we are using "Quit" in the menu or toolbar
1398 if not gl.config.on_quit_ask and gl.config.on_quit_systray and self.tray_icon and widget.name not in ('toolQuit', 'itemQuit'):
1399 self.iconify_main_window()
1400 elif gl.config.on_quit_ask or downloading:
1401 if gpodder.interface == gpodder.MAEMO:
1402 result = self.show_confirmation(_('Do you really want to quit gPodder now?'))
1403 if result:
1404 self.close_gpodder()
1405 else:
1406 return True
1407 dialog = gtk.MessageDialog(self.gPodder, gtk.DIALOG_MODAL, gtk.MESSAGE_QUESTION, gtk.BUTTONS_NONE)
1408 dialog.add_button(gtk.STOCK_CANCEL, gtk.RESPONSE_CANCEL)
1409 dialog.add_button(gtk.STOCK_QUIT, gtk.RESPONSE_CLOSE)
1411 title = _('Quit gPodder')
1412 if downloading:
1413 message = _('You are downloading episodes. If you close gPodder now, the downloads will be aborted.')
1414 else:
1415 message = _('Do you really want to quit gPodder now?')
1417 dialog.set_title(title)
1418 dialog.set_markup('<span weight="bold" size="larger">%s</span>\n\n%s'%(title, message))
1419 if not downloading:
1420 cb_ask = gtk.CheckButton(_("Don't ask me again"))
1421 dialog.vbox.pack_start(cb_ask)
1422 cb_ask.show_all()
1424 result = dialog.run()
1425 dialog.destroy()
1427 if result == gtk.RESPONSE_CLOSE:
1428 if not downloading and cb_ask.get_active() == True:
1429 gl.config.on_quit_ask = False
1430 self.close_gpodder()
1431 else:
1432 self.close_gpodder()
1434 return True
1436 def close_gpodder(self):
1437 """ clean everything and exit properly
1439 if self.channels:
1440 if not save_channels(self.channels):
1441 self.show_message(_('Please check your permissions and free disk space.'), _('Error saving podcast list'))
1443 services.download_status_manager.cancel_all()
1445 self.gtk_main_quit()
1446 sys.exit( 0)
1448 def get_old_episodes(self):
1449 episodes = []
1450 for channel in self.channels:
1451 for episode in channel.get_downloaded_episodes():
1452 if episode.is_old() and not episode.is_locked and episode.is_played:
1453 episodes.append(episode)
1454 return episodes
1456 def for_each_selected_episode_url( self, callback):
1457 ( model, paths ) = self.treeAvailable.get_selection().get_selected_rows()
1458 for path in paths:
1459 url = model.get_value( model.get_iter( path), 0)
1460 try:
1461 callback( url)
1462 except Exception, e:
1463 log( 'Warning: Error in for_each_selected_episode_url for URL %s: %s', url, e, sender = self)
1464 self.active_channel.update_model()
1465 self.updateComboBox()
1467 def delete_episode_list( self, episodes, confirm = True):
1468 if len(episodes) == 0:
1469 return
1471 if len(episodes) == 1:
1472 message = _('Do you really want to delete this episode?')
1473 else:
1474 message = _('Do you really want to delete %d episodes?') % len(episodes)
1476 if confirm and self.show_confirmation( message, _('Delete episodes')) == False:
1477 return
1479 for episode in episodes:
1480 log('Deleting episode: %s', episode.title, sender = self)
1481 episode.delete_from_disk()
1483 self.download_status_updated()
1485 def on_itemRemoveOldEpisodes_activate( self, widget):
1486 columns = (
1487 ('title', _('Episode')),
1488 ('channel_prop', _('Podcast')),
1489 ('filesize_prop', _('Size')),
1490 ('pubdate_prop', _('Released')),
1491 ('played_prop', _('Status')),
1492 ('age_prop', _('Downloaded')),
1495 selection_buttons = {
1496 _('Select played'): lambda episode: episode.is_played,
1497 _('Select older than %d days') % gl.config.episode_old_age: lambda episode: episode.is_old(),
1500 instructions = _('Select the episodes you want to delete from your hard disk.')
1502 episodes = []
1503 selected = []
1504 for channel in self.channels:
1505 for episode in channel.get_downloaded_episodes():
1506 if not episode.is_locked:
1507 episodes.append(episode)
1508 selected.append(episode.is_played)
1510 gPodderEpisodeSelector( title = _('Remove old episodes'), instructions = instructions, \
1511 episodes = episodes, selected = selected, columns = columns, \
1512 stock_ok_button = gtk.STOCK_DELETE, callback = self.delete_episode_list, \
1513 selection_buttons = selection_buttons)
1515 def mark_selected_episodes_new(self):
1516 callback = lambda url: self.active_channel.find_episode(url).mark_new()
1517 self.for_each_selected_episode_url(callback)
1519 def mark_selected_episodes_old(self):
1520 callback = lambda url: self.active_channel.find_episode(url).mark_old()
1521 self.for_each_selected_episode_url(callback)
1523 def on_item_toggle_played_activate( self, widget, toggle = True, new_value = False):
1524 if toggle:
1525 callback = lambda url: db.mark_episode(url, is_played=True, toggle=True)
1526 else:
1527 callback = lambda url: db.mark_episode(url, is_played=new_value)
1529 self.for_each_selected_episode_url(callback)
1531 def on_item_toggle_lock_activate(self, widget, toggle=True, new_value=False):
1532 if toggle:
1533 callback = lambda url: db.mark_episode(url, is_locked=True, toggle=True)
1534 else:
1535 callback = lambda url: db.mark_episode(url, is_locked=new_value)
1537 self.for_each_selected_episode_url(callback)
1539 def on_item_email_subscriptions_activate(self, widget):
1540 if not self.channels:
1541 self.show_message(_('Your subscription list is empty.'), _('Could not send list'))
1542 elif not gl.send_subscriptions():
1543 self.show_message(_('There was an error sending your subscription list via e-mail.'), _('Could not send list'))
1545 def on_item_show_url_entry_activate(self, widget):
1546 gl.config.show_podcast_url_entry = self.item_show_url_entry.get_active()
1548 def on_itemUpdate_activate(self, widget, notify_no_new_episodes=False):
1549 restore_from = can_restore_from_opml()
1551 if self.channels:
1552 self.update_feed_cache(notify_no_new_episodes=notify_no_new_episodes)
1553 elif restore_from is not None:
1554 title = _('Database upgrade required')
1555 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?')
1556 if self.show_confirmation(message, title):
1557 add_callback = lambda url: self.add_new_channel(url, False, True)
1558 w = gtk.Dialog(_('Migrating to SQLite'), self.gPodder, 0, (gtk.STOCK_CLOSE, gtk.RESPONSE_ACCEPT))
1559 w.set_has_separator(False)
1560 w.set_response_sensitive(gtk.RESPONSE_ACCEPT, False)
1561 w.set_default_size(500, -1)
1562 pb = gtk.ProgressBar()
1563 l = gtk.Label()
1564 l.set_padding(6, 3)
1565 l.set_markup('<b><big>%s</big></b>' % _('SQLite migration'))
1566 l.set_alignment(0.0, 0.5)
1567 w.vbox.pack_start(l)
1568 l = gtk.Label()
1569 l.set_padding(6, 3)
1570 l.set_alignment(0.0, 0.5)
1571 l.set_text(_('Please wait while your settings are converted.'))
1572 w.vbox.pack_start(l)
1573 w.vbox.pack_start(pb)
1574 lb = gtk.Label()
1575 lb.set_ellipsize(pango.ELLIPSIZE_END)
1576 lb.set_alignment(0.0, 0.5)
1577 lb.set_padding(6, 6)
1578 w.vbox.pack_start(lb)
1580 def set_pb_status(pb, lb, fraction, text):
1581 pb.set_fraction(float(fraction)/100.0)
1582 pb.set_text('%.0f %%' % fraction)
1583 lb.set_markup('<i>%s</i>' % saxutils.escape(text))
1584 while gtk.events_pending():
1585 gtk.main_iteration(False)
1586 status_callback = lambda fraction, text: set_pb_status(pb, lb, fraction, text)
1587 get_localdb = lambda channel: LocalDBReader(channel.url).read(channel.index_file)
1588 w.show_all()
1589 start = datetime.datetime.now()
1590 gl.migrate_to_sqlite(add_callback, status_callback, load_channels, get_localdb)
1591 # Refresh the view with the updated episodes
1592 for channel in self.channels:
1593 channel.force_update_tree_model()
1594 self.updateComboBox()
1595 time_taken = str(datetime.datetime.now()-start)
1596 status_callback(100.0, _('Migration finished in %s') % time_taken)
1597 w.set_response_sensitive(gtk.RESPONSE_ACCEPT, True)
1598 w.run()
1599 w.destroy()
1600 else:
1601 title = _('Import podcasts from the web')
1602 message = _('Your podcast list is empty. Do you want to see a list of example podcasts you can subscribe to?')
1603 if self.show_confirmation(message, title):
1604 self.on_itemImportChannels_activate(self, widget)
1606 def download_episode_list( self, episodes):
1607 services.download_status_manager.start_batch_mode()
1608 for episode in episodes:
1609 log('Downloading episode: %s', episode.title, sender = self)
1610 filename = episode.local_filename()
1611 if not os.path.exists( filename) and not services.download_status_manager.is_download_in_progress( episode.url):
1612 download.DownloadThread( episode.channel, episode, self.notification).start()
1613 services.download_status_manager.end_batch_mode()
1615 def new_episodes_show(self, episodes):
1616 columns = (
1617 ('title', _('Episode')),
1618 ('channel_prop', _('Podcast')),
1619 ('filesize_prop', _('Size')),
1620 ('pubdate_prop', _('Released')),
1623 if len(episodes) > 0:
1624 instructions = _('Select the episodes you want to download now.')
1626 gPodderEpisodeSelector(title=_('New episodes available'), instructions=instructions, \
1627 episodes=episodes, columns=columns, selected_default=True, \
1628 stock_ok_button = 'gpodder-download', \
1629 callback=self.download_episode_list)
1630 else:
1631 title = _('No new episodes')
1632 message = _('No new episodes to download.\nPlease check for new episodes later.')
1633 self.show_message(message, title)
1635 def on_itemDownloadAllNew_activate(self, widget, *args):
1636 episodes = []
1637 for channel in self.channels:
1638 for episode in channel.get_new_episodes():
1639 episodes.append(episode)
1640 self.new_episodes_show(episodes)
1642 def get_all_episodes(self, exclude_nonsignificant=True ):
1643 """'exclude_nonsignificant' will exclude non-downloaded episodes
1644 and all episodes from channels that are set to skip when syncing"""
1645 episode_list = []
1646 for channel in self.channels:
1647 if not channel.sync_to_devices and exclude_nonsignificant:
1648 log('Skipping channel: %s', channel.title, sender=self)
1649 continue
1650 for episode in channel.get_all_episodes():
1651 if episode.was_downloaded(and_exists=True) or not exclude_nonsignificant:
1652 episode_list.append(episode)
1653 return episode_list
1655 def ipod_delete_played(self, device):
1656 all_episodes = self.get_all_episodes( exclude_nonsignificant=False )
1657 episodes_on_device = device.get_all_tracks()
1658 for local_episode in all_episodes:
1659 if local_episode.is_played and not local_episode.is_locked or local_episode.is_deleted:
1660 if gl.config.device_type == 'filesystem':
1661 local_episode_name = util.sanitize_filename(local_episode.sync_filename(), gl.config.mp3_player_max_filename_length)
1662 else:
1663 local_episode_name = local_episode.sync_filename()
1664 for device_episode in episodes_on_device:
1665 if device_episode.title == local_episode_name:
1666 log("mp3_player_delete_played: removing %s" % device_episode.title)
1667 device.remove_track(device_episode)
1668 break
1670 def on_sync_to_ipod_activate(self, widget, episodes=None):
1671 Thread(target=self.sync_to_ipod_thread, args=(widget, episodes)).start()
1673 def sync_to_ipod_thread(self, widget, episodes=None):
1674 device = sync.open_device()
1676 if device is None:
1677 title = _('No device configured')
1678 message = _('To use the synchronization feature, please configure your device in the preferences dialog first.')
1679 self.notification(message, title)
1680 return
1682 if not device.open():
1683 title = _('Cannot open device')
1684 message = _('There has been an error opening your device.')
1685 self.notification(message, title)
1686 return
1688 gPodderSync(device=device, gPodder=self)
1689 if self.tray_icon:
1690 self.tray_icon.set_synchronisation_device(device)
1692 if episodes is None:
1693 episodes_to_sync = self.get_all_episodes()
1694 device.add_tracks(episodes_to_sync)
1695 # 'only_sync_not_played' must be used or else all the played
1696 # tracks will be copied then immediately deleted
1697 if gl.config.mp3_player_delete_played and gl.config.only_sync_not_played:
1698 self.ipod_delete_played(device)
1699 else:
1700 device.add_tracks(episodes, force_played=True)
1702 if not device.close():
1703 title = _('Error closing device')
1704 message = _('There has been an error closing your device.')
1705 self.notification(message, title)
1706 return
1708 if self.tray_icon:
1709 self.tray_icon.release_synchronisation_device()
1711 # update model for played state updates after sync
1712 for channel in self.channels:
1713 util.idle_add(channel.update_model)
1714 util.idle_add(self.updateComboBox)
1716 def ipod_cleanup_callback(self, device, tracks):
1717 title = _('Delete podcasts from device?')
1718 message = _('Do you really want to completely remove the selected episodes?')
1719 if len(tracks) > 0 and self.show_confirmation(message, title):
1720 device.remove_tracks(tracks)
1722 if not device.close():
1723 title = _('Error closing device')
1724 message = _('There has been an error closing your device.')
1725 self.show_message(message, title)
1726 return
1728 def on_cleanup_ipod_activate(self, widget, *args):
1729 columns = (
1730 ('title', _('Episode')),
1731 ('podcast', _('Podcast')),
1732 ('filesize', _('Size')),
1733 ('modified', _('Copied')),
1734 ('playcount', _('Play count')),
1735 ('released', _('Released')),
1738 device = sync.open_device()
1740 if device is None:
1741 title = _('No device configured')
1742 message = _('To use the synchronization feature, please configure your device in the preferences dialog first.')
1743 self.show_message(message, title)
1744 return
1746 if not device.open():
1747 title = _('Cannot open device')
1748 message = _('There has been an error opening your device.')
1749 self.show_message(message, title)
1750 return
1752 gPodderSync(device=device, gPodder=self)
1754 tracks = device.get_all_tracks()
1755 if len(tracks) > 0:
1756 remove_tracks_callback = lambda tracks: self.ipod_cleanup_callback(device, tracks)
1757 wanted_columns = []
1758 for key, caption in columns:
1759 want_this_column = False
1760 for track in tracks:
1761 if getattr(track, key) is not None:
1762 want_this_column = True
1763 break
1765 if want_this_column:
1766 wanted_columns.append((key, caption))
1767 title = _('Remove podcasts from device')
1768 instructions = _('Select the podcast episodes you want to remove from your device.')
1769 gPodderEpisodeSelector(title=title, instructions=instructions, episodes=tracks, columns=wanted_columns, \
1770 stock_ok_button=gtk.STOCK_DELETE, callback=remove_tracks_callback)
1771 else:
1772 title = _('No files on device')
1773 message = _('The devices contains no files to be removed.')
1774 self.show_message(message, title)
1776 def show_hide_tray_icon(self):
1777 if gl.config.display_tray_icon and have_trayicon and self.tray_icon is None:
1778 self.tray_icon = trayicon.GPodderStatusIcon(self, scalable_dir)
1779 elif not gl.config.display_tray_icon and self.tray_icon is not None:
1780 self.tray_icon.set_visible(False)
1781 del self.tray_icon
1782 self.tray_icon = None
1784 if gl.config.minimize_to_tray and self.tray_icon:
1785 self.tray_icon.set_visible(self.minimized)
1786 elif self.tray_icon:
1787 self.tray_icon.set_visible(True)
1789 def on_itemShowToolbar_activate(self, widget):
1790 gl.config.show_toolbar = self.itemShowToolbar.get_active()
1792 def on_itemShowDescription_activate(self, widget):
1793 gl.config.episode_list_descriptions = self.itemShowDescription.get_active()
1795 def update_item_device( self):
1796 if gl.config.device_type != 'none':
1797 self.itemDevice.show_all()
1798 (label,) = self.itemDevice.get_children()
1799 label.set_text(gl.get_device_name())
1800 else:
1801 self.itemDevice.hide_all()
1803 def properties_closed( self):
1804 self.show_hide_tray_icon()
1805 self.update_item_device()
1806 self.updateComboBox()
1808 def on_itemPreferences_activate(self, widget, *args):
1809 if gpodder.interface == gpodder.GUI:
1810 gPodderProperties(callback_finished=self.properties_closed, user_apps_reader=self.user_apps_reader)
1811 else:
1812 gPodderMaemoPreferences()
1814 def on_itemAddChannel_activate(self, widget, *args):
1815 if gpodder.interface == gpodder.MAEMO or not gl.config.show_podcast_url_entry:
1816 gPodderAddPodcastDialog(url_callback=self.add_new_channel)
1817 else:
1818 if self.channelPaned.get_position() < 200:
1819 self.channelPaned.set_position( 200)
1820 self.entryAddChannel.grab_focus()
1822 def on_itemEditChannel_activate(self, widget, *args):
1823 if self.active_channel is None:
1824 title = _('No podcast selected')
1825 message = _('Please select a podcast in the podcasts list to edit.')
1826 self.show_message( message, title)
1827 return
1829 gPodderChannel(channel=self.active_channel, callback_closed=self.updateComboBox, callback_change_url=self.change_channel_url)
1831 def change_channel_url(self, old_url, new_url):
1832 channel = None
1833 try:
1834 channel = podcastChannel.load(url=new_url, create=True)
1835 except:
1836 channel = None
1838 if channel is None:
1839 self.show_message(_('The specified URL is invalid. The old URL has been used instead.'), _('Invalid URL'))
1840 return
1842 for channel in self.channels:
1843 if channel.url == old_url:
1844 log('=> change channel url from %s to %s', old_url, new_url)
1845 old_save_dir = channel.save_dir
1846 channel.url = new_url
1847 new_save_dir = channel.save_dir
1848 log('old save dir=%s', old_save_dir, sender=self)
1849 log('new save dir=%s', new_save_dir, sender=self)
1850 files = glob.glob(os.path.join(old_save_dir, '*'))
1851 log('moving %d files to %s', len(files), new_save_dir, sender=self)
1852 for file in files:
1853 log('moving %s', file, sender=self)
1854 shutil.move(file, new_save_dir)
1855 try:
1856 os.rmdir(old_save_dir)
1857 except:
1858 log('Warning: cannot delete %s', old_save_dir, sender=self)
1860 save_channels(self.channels)
1861 # update feed cache and select the podcast with the new URL afterwards
1862 self.update_feed_cache(force_update=False, select_url_afterwards=new_url)
1864 def on_itemRemoveChannel_activate(self, widget, *args):
1865 try:
1866 if gpodder.interface == gpodder.GUI:
1867 dialog = gtk.MessageDialog(self.gPodder, gtk.DIALOG_MODAL, gtk.MESSAGE_QUESTION, gtk.BUTTONS_NONE)
1868 dialog.add_button(gtk.STOCK_NO, gtk.RESPONSE_NO)
1869 dialog.add_button(gtk.STOCK_YES, gtk.RESPONSE_YES)
1871 title = _('Remove podcast and episodes?')
1872 message = _('Do you really want to remove <b>%s</b> and all downloaded episodes?') % saxutils.escape(self.active_channel.title)
1874 dialog.set_title(title)
1875 dialog.set_markup('<span weight="bold" size="larger">%s</span>\n\n%s'%(title, message))
1877 cb_ask = gtk.CheckButton(_('Do not delete my downloaded episodes'))
1878 dialog.vbox.pack_start(cb_ask)
1879 cb_ask.show_all()
1880 affirmative = gtk.RESPONSE_YES
1881 elif gpodder.interface == gpodder.MAEMO:
1882 cb_ask = gtk.CheckButton('') # dummy check button
1883 dialog = hildon.Note('confirmation', (self.gPodder, _('Do you really want to remove this podcast and all downloaded episodes?')))
1884 affirmative = gtk.RESPONSE_OK
1886 result = dialog.run()
1887 dialog.destroy()
1889 if result == affirmative:
1890 # delete downloaded episodes only if checkbox is unchecked
1891 if cb_ask.get_active() == False:
1892 self.active_channel.remove_downloaded()
1893 else:
1894 log('Not removing downloaded episodes', sender=self)
1896 # only delete partial files if we do not have any downloads in progress
1897 delete_partial = not services.download_status_manager.has_items()
1898 gl.clean_up_downloads(delete_partial)
1900 # get the URL of the podcast we want to select next
1901 position = self.channels.index(self.active_channel)
1902 if position == len(self.channels)-1:
1903 # this is the last podcast, so select the URL
1904 # of the item before this one (i.e. the "new last")
1905 select_url = self.channels[position-1].url
1906 else:
1907 # there is a podcast after the deleted one, so
1908 # we simply select the one that comes after it
1909 select_url = self.channels[position+1].url
1911 # Remove the channel
1912 self.active_channel.delete()
1913 self.channels.remove(self.active_channel)
1914 save_channels(self.channels)
1916 # Re-load the channels and select the desired new channel
1917 self.update_feed_cache(force_update=False, select_url_afterwards=select_url)
1918 except:
1919 log('There has been an error removing the channel.', traceback=True, sender=self)
1921 def get_opml_filter(self):
1922 filter = gtk.FileFilter()
1923 filter.add_pattern('*.opml')
1924 filter.add_pattern('*.xml')
1925 filter.set_name(_('OPML files')+' (*.opml, *.xml)')
1926 return filter
1928 def on_item_import_from_file_activate(self, widget, filename=None):
1929 if filename is None:
1930 if gpodder.interface == gpodder.GUI:
1931 dlg = gtk.FileChooserDialog(title=_('Import from OPML'), parent=None, action=gtk.FILE_CHOOSER_ACTION_OPEN)
1932 dlg.add_button(gtk.STOCK_CANCEL, gtk.RESPONSE_CANCEL)
1933 dlg.add_button(gtk.STOCK_OPEN, gtk.RESPONSE_OK)
1934 elif gpodder.interface == gpodder.MAEMO:
1935 dlg = hildon.FileChooserDialog(self.gPodder, gtk.FILE_CHOOSER_ACTION_OPEN)
1936 dlg.set_filter(self.get_opml_filter())
1937 response = dlg.run()
1938 filename = None
1939 if response == gtk.RESPONSE_OK:
1940 filename = dlg.get_filename()
1941 dlg.destroy()
1943 if filename is not None:
1944 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), lambda: self.on_itemDownloadAllNew_activate(self.gPodder))
1946 def on_itemExportChannels_activate(self, widget, *args):
1947 if not self.channels:
1948 title = _('Nothing to export')
1949 message = _('Your list of podcast subscriptions is empty. Please subscribe to some podcasts first before trying to export your subscription list.')
1950 self.show_message( message, title)
1951 return
1953 if gpodder.interface == gpodder.GUI:
1954 dlg = gtk.FileChooserDialog(title=_('Export to OPML'), parent=self.gPodder, action=gtk.FILE_CHOOSER_ACTION_SAVE)
1955 dlg.add_button(gtk.STOCK_CANCEL, gtk.RESPONSE_CANCEL)
1956 dlg.add_button(gtk.STOCK_SAVE, gtk.RESPONSE_OK)
1957 elif gpodder.interface == gpodder.MAEMO:
1958 dlg = hildon.FileChooserDialog(self.gPodder, gtk.FILE_CHOOSER_ACTION_SAVE)
1959 dlg.set_filter(self.get_opml_filter())
1960 response = dlg.run()
1961 if response == gtk.RESPONSE_OK:
1962 filename = dlg.get_filename()
1963 exporter = opml.Exporter( filename)
1964 if not exporter.write( self.channels):
1965 self.show_message( _('Could not export OPML to file. Please check your permissions.'), _('OPML export failed'))
1967 dlg.destroy()
1969 def on_itemImportChannels_activate(self, widget, *args):
1970 gPodderOpmlLister().get_channels_from_url(gl.config.opml_url, lambda url: self.add_new_channel(url,False), lambda: self.on_itemDownloadAllNew_activate(self.gPodder))
1972 def on_btnTransfer_clicked(self, widget, *args):
1973 self.on_treeAvailable_row_activated( widget, args)
1975 def on_homepage_activate(self, widget, *args):
1976 util.open_website(app_website)
1978 def on_wiki_activate(self, widget, *args):
1979 util.open_website('http://wiki.gpodder.org/')
1981 def on_bug_tracker_activate(self, widget, *args):
1982 util.open_website('http://bugs.gpodder.org/')
1984 def on_itemAbout_activate(self, widget, *args):
1985 dlg = gtk.AboutDialog()
1986 dlg.set_name(app_name.replace('p', 'P')) # gpodder->gPodder
1987 dlg.set_version( app_version)
1988 dlg.set_copyright( app_copyright)
1989 dlg.set_website( app_website)
1990 dlg.set_translator_credits( _('translator-credits'))
1991 dlg.connect( 'response', lambda dlg, response: dlg.destroy())
1993 if gpodder.interface == gpodder.GUI:
1994 # For the "GUI" version, we add some more
1995 # items to the about dialog (credits and logo)
1996 dlg.set_authors(app_authors)
1997 try:
1998 dlg.set_logo(gtk.gdk.pixbuf_new_from_file_at_size(scalable_dir, 200, 200))
1999 except:
2000 pass
2002 dlg.run()
2004 def on_wNotebook_switch_page(self, widget, *args):
2005 page_num = args[1]
2006 if gpodder.interface == gpodder.MAEMO:
2007 page = self.wNotebook.get_nth_page(page_num)
2008 tab_label = self.wNotebook.get_tab_label(page).get_text()
2009 if page_num == 0 and self.active_channel is not None:
2010 self.set_title(self.active_channel.title)
2011 else:
2012 self.set_title(tab_label)
2013 if page_num == 0:
2014 self.play_or_download()
2015 else:
2016 self.toolDownload.set_sensitive( False)
2017 self.toolPlay.set_sensitive( False)
2018 self.toolTransfer.set_sensitive( False)
2019 self.toolCancel.set_sensitive( services.download_status_manager.has_items())
2021 def on_treeChannels_row_activated(self, widget, *args):
2022 self.on_itemEditChannel_activate( self.treeChannels)
2024 def on_treeChannels_cursor_changed(self, widget, *args):
2025 ( model, iter ) = self.treeChannels.get_selection().get_selected()
2027 if model is not None and iter != None:
2028 id = model.get_path( iter)[0]
2029 self.active_channel = self.channels[id]
2031 if gpodder.interface == gpodder.MAEMO:
2032 self.set_title(self.active_channel.title)
2033 self.itemEditChannel.show_all()
2034 self.itemRemoveChannel.show_all()
2035 else:
2036 self.active_channel = None
2037 self.itemEditChannel.hide_all()
2038 self.itemRemoveChannel.hide_all()
2040 self.updateTreeView()
2042 def on_entryAddChannel_changed(self, widget, *args):
2043 active = self.entryAddChannel.get_text() not in ('', self.ENTER_URL_TEXT)
2044 self.btnAddChannel.set_sensitive( active)
2046 def on_btnAddChannel_clicked(self, widget, *args):
2047 url = self.entryAddChannel.get_text()
2048 self.entryAddChannel.set_text('')
2049 self.add_new_channel( url)
2051 def on_btnEditChannel_clicked(self, widget, *args):
2052 self.on_itemEditChannel_activate( widget, args)
2054 def on_treeAvailable_row_activated(self, widget, *args):
2055 try:
2056 selection = self.treeAvailable.get_selection()
2057 selection_tuple = selection.get_selected_rows()
2058 transfer_files = False
2059 episodes = []
2061 if selection.count_selected_rows() > 1:
2062 widget_to_send = None
2063 show_message_dialog = False
2064 else:
2065 widget_to_send = widget
2066 show_message_dialog = True
2068 if widget.get_name() == 'itemTransferSelected' or widget.get_name() == 'toolTransfer':
2069 transfer_files = True
2071 services.download_status_manager.start_batch_mode()
2072 for apath in selection_tuple[1]:
2073 selection_iter = self.treeAvailable.get_model().get_iter( apath)
2074 url = self.treeAvailable.get_model().get_value( selection_iter, 0)
2076 if transfer_files:
2077 episodes.append( self.active_channel.find_episode( url))
2078 else:
2079 self.download_podcast_by_url( url, show_message_dialog, widget_to_send)
2080 services.download_status_manager.end_batch_mode()
2082 if transfer_files and len(episodes):
2083 self.on_sync_to_ipod_activate(None, episodes)
2084 except:
2085 title = _('Nothing selected')
2086 message = _('Please select an episode that you want to download and then click on the download button to start downloading the selected episode.')
2087 self.show_message( message, title)
2089 def on_btnDownload_clicked(self, widget, *args):
2090 self.on_treeAvailable_row_activated( widget, args)
2092 def on_treeAvailable_button_release_event(self, widget, *args):
2093 self.play_or_download()
2095 def auto_update_procedure(self, first_run=False):
2096 log('auto_update_procedure() got called', sender=self)
2097 if not first_run and gl.config.auto_update_feeds and self.minimized:
2098 self.update_feed_cache(force_update=True)
2100 next_update = 60*1000*gl.config.auto_update_frequency
2101 gobject.timeout_add(next_update, self.auto_update_procedure)
2103 def on_treeDownloads_row_activated(self, widget, *args):
2104 cancel_urls = []
2106 if self.wNotebook.get_current_page() > 0:
2107 # Use the download list treeview + model
2108 ( tree, column ) = ( self.treeDownloads, 3 )
2109 else:
2110 # Use the available podcasts treeview + model
2111 ( tree, column ) = ( self.treeAvailable, 0 )
2113 selection = tree.get_selection()
2114 (model, paths) = selection.get_selected_rows()
2115 for path in paths:
2116 url = model.get_value( model.get_iter( path), column)
2117 cancel_urls.append( url)
2119 if len( cancel_urls) == 0:
2120 log('Nothing selected.', sender = self)
2121 return
2123 if len( cancel_urls) == 1:
2124 title = _('Cancel download?')
2125 message = _("Cancelling this download will remove the partially downloaded file and stop the download.")
2126 else:
2127 title = _('Cancel downloads?')
2128 message = _("Cancelling the download will stop the %d selected downloads and remove partially downloaded files.") % selection.count_selected_rows()
2130 if self.show_confirmation( message, title):
2131 services.download_status_manager.start_batch_mode()
2132 for url in cancel_urls:
2133 services.download_status_manager.cancel_by_url( url)
2134 services.download_status_manager.end_batch_mode()
2136 def on_btnCancelDownloadStatus_clicked(self, widget, *args):
2137 self.on_treeDownloads_row_activated( widget, None)
2139 def on_btnCancelAll_clicked(self, widget, *args):
2140 self.treeDownloads.get_selection().select_all()
2141 self.on_treeDownloads_row_activated( self.toolCancel, None)
2142 self.treeDownloads.get_selection().unselect_all()
2144 def on_btnDownloadedExecute_clicked(self, widget, *args):
2145 self.on_treeAvailable_row_activated( widget, args)
2147 def on_btnDownloadedDelete_clicked(self, widget, *args):
2148 if self.active_channel is None:
2149 return
2151 channel_url = self.active_channel.url
2152 selection = self.treeAvailable.get_selection()
2153 ( model, paths ) = selection.get_selected_rows()
2155 if selection.count_selected_rows() == 0:
2156 log( 'Nothing selected - will not remove any downloaded episode.')
2157 return
2159 if selection.count_selected_rows() == 1:
2160 episode_title = saxutils.escape(model.get_value(model.get_iter(paths[0]), 1))
2162 episode = db.load_episode(model.get_value(model.get_iter(paths[0]), 0))
2163 if episode['is_locked']:
2164 title = _('%s is locked') % episode_title
2165 message = _('You cannot delete this locked episode. You must unlock it before you can delete it.')
2166 self.notification(message, title)
2167 return
2169 title = _('Remove %s?') % episode_title
2170 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.")
2171 else:
2172 title = _('Remove %d episodes?') % selection.count_selected_rows()
2173 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.')
2175 locked_count = 0
2176 for path in paths:
2177 episode = db.load_episode(model.get_value(model.get_iter(path), 0))
2178 if episode['is_locked']:
2179 locked_count += 1
2181 if selection.count_selected_rows() == locked_count:
2182 title = _('Episodes are locked')
2183 message = _('The selected episodes are locked. Please unlock the episodes that you want to delete before trying to delete them.')
2184 self.notification(message, title)
2185 return
2186 elif locked_count > 0:
2187 title = _('Remove %d out of %d episodes?') % (selection.count_selected_rows() - locked_count, selection.count_selected_rows())
2188 message = _('The selection contains locked episodes. These will not be deleted. If you want to listen to any of these episodes again, then you will have to re-download them.')
2190 # if user confirms deletion, let's remove some stuff ;)
2191 if self.show_confirmation( message, title):
2192 try:
2193 # iterate over the selection, see also on_treeDownloads_row_activated
2194 for path in paths:
2195 url = model.get_value( model.get_iter( path), 0)
2196 self.active_channel.delete_episode_by_url( url)
2198 # now, clear local db cache so we can re-read it
2199 self.updateComboBox()
2200 except:
2201 log( 'Error while deleting (some) downloads.')
2203 # only delete partial files if we do not have any downloads in progress
2204 delete_partial = not services.download_status_manager.has_items()
2205 gl.clean_up_downloads(delete_partial)
2206 self.active_channel.force_update_tree_model()
2207 self.updateTreeView()
2209 def on_key_press(self, widget, event):
2210 # Currently, we only handle Maemo hardware keys here,
2211 # so if we are not a Maemo app, we don't do anything!
2212 if gpodder.interface != gpodder.MAEMO:
2213 return
2215 if event.keyval == gtk.keysyms.F6:
2216 if self.fullscreen:
2217 self.window.unfullscreen()
2218 else:
2219 self.window.fullscreen()
2220 if event.keyval == gtk.keysyms.Escape:
2221 new_visibility = not self.vboxChannelNavigator.get_property('visible')
2222 self.vboxChannelNavigator.set_property('visible', new_visibility)
2223 self.column_size.set_visible(not new_visibility)
2224 self.column_released.set_visible(not new_visibility)
2226 diff = 0
2227 if event.keyval == gtk.keysyms.F7: #plus
2228 diff = 1
2229 elif event.keyval == gtk.keysyms.F8: #minus
2230 diff = -1
2232 if diff != 0:
2233 selection = self.treeChannels.get_selection()
2234 (model, iter) = selection.get_selected()
2235 selection.select_path(((model.get_path(iter)[0]+diff)%len(model),))
2236 self.on_treeChannels_cursor_changed(self.treeChannels)
2238 def window_state_event(self, widget, event):
2239 if event.new_window_state & gtk.gdk.WINDOW_STATE_FULLSCREEN:
2240 self.fullscreen = True
2241 else:
2242 self.fullscreen = False
2244 old_minimized = self.minimized
2246 if event.new_window_state & gtk.gdk.WINDOW_STATE_ICONIFIED:
2247 self.minimized = True
2248 else:
2249 self.minimized = False
2251 if old_minimized != self.minimized and self.tray_icon:
2252 self.gPodder.set_skip_taskbar_hint(self.minimized)
2253 elif not self.tray_icon:
2254 self.gPodder.set_skip_taskbar_hint(False)
2256 if gl.config.minimize_to_tray and self.tray_icon:
2257 self.tray_icon.set_visible(self.minimized)
2259 def uniconify_main_window(self):
2260 if self.minimized:
2261 self.gPodder.present()
2263 def iconify_main_window(self):
2264 if not self.minimized:
2265 self.gPodder.iconify()
2267 class gPodderChannel(GladeWidget):
2268 finger_friendly_widgets = ['btn_website', 'btnOK', 'channel_description']
2270 def new(self):
2271 global WEB_BROWSER_ICON
2272 self.changed = False
2273 self.image3167.set_property('icon-name', WEB_BROWSER_ICON)
2274 self.gPodderChannel.set_title( self.channel.title)
2275 self.entryTitle.set_text( self.channel.title)
2276 self.entryURL.set_text( self.channel.url)
2278 self.LabelDownloadTo.set_text( self.channel.save_dir)
2279 self.LabelWebsite.set_text( self.channel.link)
2281 self.cbNoSync.set_active( not self.channel.sync_to_devices)
2282 self.musicPlaylist.set_text(self.channel.device_playlist_name)
2283 if self.channel.username:
2284 self.FeedUsername.set_text( self.channel.username)
2285 if self.channel.password:
2286 self.FeedPassword.set_text( self.channel.password)
2288 services.cover_downloader.register('cover-available', self.cover_download_finished)
2289 services.cover_downloader.request_cover(self.channel)
2291 # Hide the website button if we don't have a valid URL
2292 if not self.channel.link:
2293 self.btn_website.hide_all()
2295 b = gtk.TextBuffer()
2296 b.set_text( self.channel.description)
2297 self.channel_description.set_buffer( b)
2299 #Add Drag and Drop Support
2300 flags = gtk.DEST_DEFAULT_ALL
2301 targets = [ ('text/uri-list', 0, 2), ('text/plain', 0, 4) ]
2302 actions = gtk.gdk.ACTION_DEFAULT | gtk.gdk.ACTION_COPY
2303 self.vboxCoverEditor.drag_dest_set( flags, targets, actions)
2304 self.vboxCoverEditor.connect( 'drag_data_received', self.drag_data_received)
2306 def on_btn_website_clicked(self, widget):
2307 util.open_website(self.channel.link)
2309 def on_btnDownloadCover_clicked(self, widget):
2310 if gpodder.interface == gpodder.GUI:
2311 dlg = gtk.FileChooserDialog(title=_('Select new podcast cover artwork'), parent=self.gPodderChannel, action=gtk.FILE_CHOOSER_ACTION_OPEN)
2312 dlg.add_button(gtk.STOCK_CANCEL, gtk.RESPONSE_CANCEL)
2313 dlg.add_button(gtk.STOCK_OPEN, gtk.RESPONSE_OK)
2314 elif gpodder.interface == gpodder.MAEMO:
2315 dlg = hildon.FileChooserDialog(self.gPodderChannel, gtk.FILE_CHOOSER_ACTION_OPEN)
2317 if dlg.run() == gtk.RESPONSE_OK:
2318 url = dlg.get_uri()
2319 services.cover_downloader.replace_cover(self.channel, url)
2321 dlg.destroy()
2323 def on_btnClearCover_clicked(self, widget):
2324 services.cover_downloader.replace_cover(self.channel)
2326 def cover_download_finished(self, channel_url, pixbuf):
2327 if pixbuf is not None:
2328 self.imgCover.set_from_pixbuf(pixbuf)
2329 self.gPodderChannel.show()
2331 def drag_data_received( self, widget, content, x, y, sel, ttype, time):
2332 files = sel.data.strip().split('\n')
2333 if len(files) != 1:
2334 self.show_message( _('You can only drop a single image or URL here.'), _('Drag and drop'))
2335 return
2337 file = files[0]
2339 if file.startswith('file://') or file.startswith('http://'):
2340 services.cover_downloader.replace_cover(self.channel, file)
2341 return
2343 self.show_message( _('You can only drop local files and http:// URLs here.'), _('Drag and drop'))
2345 def on_gPodderChannel_destroy(self, widget, *args):
2346 services.cover_downloader.unregister('cover-available', self.cover_download_finished)
2348 def on_btnOK_clicked(self, widget, *args):
2349 entered_url = self.entryURL.get_text()
2350 channel_url = self.channel.url
2352 if entered_url != channel_url:
2353 if self.show_confirmation(_('Do you really want to move this podcast to <b>%s</b>?') % (saxutils.escape(entered_url),), _('Really change URL?')):
2354 if hasattr(self, 'callback_change_url'):
2355 self.gPodderChannel.hide_all()
2356 self.callback_change_url(channel_url, entered_url)
2358 self.channel.sync_to_devices = not self.cbNoSync.get_active()
2359 self.channel.device_playlist_name = self.musicPlaylist.get_text()
2360 self.channel.set_custom_title( self.entryTitle.get_text())
2361 self.channel.username = self.FeedUsername.get_text().strip()
2362 self.channel.password = self.FeedPassword.get_text()
2363 self.channel.save()
2365 self.gPodderChannel.destroy()
2366 self.callback_closed()
2368 class gPodderAddPodcastDialog(GladeWidget):
2369 finger_friendly_widgets = ['btn_close', 'btn_add']
2371 def new(self):
2372 if not hasattr(self, 'url_callback'):
2373 log('No url callback set', sender=self)
2374 self.url_callback = None
2376 def on_btn_close_clicked(self, widget):
2377 self.gPodderAddPodcastDialog.destroy()
2379 def on_entry_url_changed(self, widget):
2380 self.btn_add.set_sensitive(self.entry_url.get_text().strip() != '')
2382 def on_btn_add_clicked(self, widget):
2383 url = self.entry_url.get_text()
2384 self.on_btn_close_clicked(widget)
2385 if self.url_callback is not None:
2386 self.url_callback(url)
2389 class gPodderMaemoPreferences(GladeWidget):
2390 finger_friendly_widgets = ['btn_close', 'label128', 'label129', 'btn_advanced']
2392 def new(self):
2393 gl.config.connect_gtk_togglebutton('update_on_startup', self.update_on_startup)
2394 gl.config.connect_gtk_togglebutton('display_tray_icon', self.show_tray_icon)
2395 gl.config.connect_gtk_togglebutton('enable_notifications', self.show_notifications)
2396 gl.config.connect_gtk_togglebutton('on_quit_ask', self.on_quit_ask)
2398 self.restart_required = False
2399 self.show_tray_icon.connect('clicked', self.on_restart_required)
2400 self.show_notifications.connect('clicked', self.on_restart_required)
2402 def on_restart_required(self, widget):
2403 self.restart_required = True
2405 def on_btn_advanced_clicked(self, widget):
2406 self.gPodderMaemoPreferences.destroy()
2407 gPodderConfigEditor()
2409 def on_btn_close_clicked(self, widget):
2410 self.gPodderMaemoPreferences.destroy()
2411 if self.restart_required:
2412 self.show_message(_('Please restart gPodder for the changes to take effect.'))
2415 class gPodderProperties(GladeWidget):
2416 def new(self):
2417 if not hasattr( self, 'callback_finished'):
2418 self.callback_finished = None
2420 if gpodder.interface == gpodder.MAEMO:
2421 self.table13.hide_all() # bluetooth
2422 self.table5.hide_all() # player
2423 self.table6.hide_all() # bittorrent
2424 self.gPodderProperties.fullscreen()
2426 gl.config.connect_gtk_editable( 'http_proxy', self.httpProxy)
2427 gl.config.connect_gtk_editable( 'ftp_proxy', self.ftpProxy)
2428 gl.config.connect_gtk_editable( 'player', self.openApp)
2429 gl.config.connect_gtk_editable('videoplayer', self.openVideoApp)
2430 gl.config.connect_gtk_editable( 'custom_sync_name', self.entryCustomSyncName)
2431 gl.config.connect_gtk_togglebutton( 'custom_sync_name_enabled', self.cbCustomSyncName)
2432 gl.config.connect_gtk_togglebutton( 'auto_download_when_minimized', self.downloadnew)
2433 gl.config.connect_gtk_togglebutton( 'use_gnome_bittorrent', self.radio_gnome_bittorrent)
2434 gl.config.connect_gtk_togglebutton( 'update_on_startup', self.updateonstartup)
2435 gl.config.connect_gtk_togglebutton( 'only_sync_not_played', self.only_sync_not_played)
2436 gl.config.connect_gtk_togglebutton( 'fssync_channel_subfolders', self.cbChannelSubfolder)
2437 gl.config.connect_gtk_togglebutton( 'on_sync_mark_played', self.on_sync_mark_played)
2438 gl.config.connect_gtk_togglebutton( 'on_sync_delete', self.on_sync_delete)
2439 gl.config.connect_gtk_togglebutton( 'proxy_use_environment', self.cbEnvironmentVariables)
2440 gl.config.connect_gtk_filechooser( 'bittorrent_dir', self.chooserBitTorrentTo)
2441 gl.config.connect_gtk_spinbutton('episode_old_age', self.episode_old_age)
2442 gl.config.connect_gtk_togglebutton('auto_remove_old_episodes', self.auto_remove_old_episodes)
2443 gl.config.connect_gtk_togglebutton('auto_update_feeds', self.auto_update_feeds)
2444 gl.config.connect_gtk_spinbutton('auto_update_frequency', self.auto_update_frequency)
2445 gl.config.connect_gtk_togglebutton('display_tray_icon', self.display_tray_icon)
2446 gl.config.connect_gtk_togglebutton('minimize_to_tray', self.minimize_to_tray)
2447 gl.config.connect_gtk_togglebutton('enable_notifications', self.enable_notifications)
2448 gl.config.connect_gtk_togglebutton('start_iconified', self.start_iconified)
2449 gl.config.connect_gtk_togglebutton('bluetooth_enabled', self.bluetooth_enabled)
2450 gl.config.connect_gtk_togglebutton('bluetooth_ask_always', self.bluetooth_ask_always)
2451 gl.config.connect_gtk_togglebutton('bluetooth_ask_never', self.bluetooth_ask_never)
2452 gl.config.connect_gtk_togglebutton('bluetooth_use_converter', self.bluetooth_use_converter)
2453 gl.config.connect_gtk_filechooser( 'bluetooth_converter', self.bluetooth_converter, is_for_files=True)
2454 gl.config.connect_gtk_togglebutton('ipod_write_gtkpod_extended', self.ipod_write_gtkpod_extended)
2455 gl.config.connect_gtk_togglebutton('mp3_player_delete_played', self.delete_episodes_marked_played)
2457 self.enable_notifications.set_sensitive(self.display_tray_icon.get_active())
2458 self.minimize_to_tray.set_sensitive(self.display_tray_icon.get_active())
2460 self.entryCustomSyncName.set_sensitive( self.cbCustomSyncName.get_active())
2462 self.radio_gnome_bittorrent.set_active(gl.config.use_gnome_bittorrent)
2463 self.radio_copy_torrents.set_active(not gl.config.use_gnome_bittorrent)
2465 self.iPodMountpoint.set_label( gl.config.ipod_mount)
2466 self.filesystemMountpoint.set_label( gl.config.mp3_player_folder)
2467 self.bluetooth_device_name.set_markup('<b>%s</b>'%gl.config.bluetooth_device_name)
2468 self.chooserDownloadTo.set_current_folder(gl.downloaddir)
2470 self.on_sync_delete.set_sensitive(not self.delete_episodes_marked_played.get_active())
2471 self.on_sync_mark_played.set_sensitive(not self.delete_episodes_marked_played.get_active())
2473 if tagging_supported():
2474 gl.config.connect_gtk_togglebutton( 'update_tags', self.updatetags)
2475 else:
2476 self.updatetags.set_sensitive( False)
2477 new_label = '%s (%s)' % ( self.updatetags.get_label(), _('needs python-eyed3') )
2478 self.updatetags.set_label( new_label)
2480 # device type
2481 self.comboboxDeviceType.set_active( 0)
2482 if gl.config.device_type == 'ipod':
2483 self.comboboxDeviceType.set_active( 1)
2484 elif gl.config.device_type == 'filesystem':
2485 self.comboboxDeviceType.set_active( 2)
2487 # setup cell renderers
2488 cellrenderer = gtk.CellRendererPixbuf()
2489 self.comboAudioPlayerApp.pack_start(cellrenderer, False)
2490 self.comboAudioPlayerApp.add_attribute(cellrenderer, 'pixbuf', 2)
2491 cellrenderer = gtk.CellRendererText()
2492 self.comboAudioPlayerApp.pack_start(cellrenderer, True)
2493 self.comboAudioPlayerApp.add_attribute(cellrenderer, 'markup', 0)
2495 cellrenderer = gtk.CellRendererPixbuf()
2496 self.comboVideoPlayerApp.pack_start(cellrenderer, False)
2497 self.comboVideoPlayerApp.add_attribute(cellrenderer, 'pixbuf', 2)
2498 cellrenderer = gtk.CellRendererText()
2499 self.comboVideoPlayerApp.pack_start(cellrenderer, True)
2500 self.comboVideoPlayerApp.add_attribute(cellrenderer, 'markup', 0)
2502 if not hasattr(self, 'user_apps_reader'):
2503 self.user_apps_reader = UserAppsReader(['audio', 'video'])
2505 if gpodder.interface == gpodder.GUI:
2506 self.user_apps_reader.read()
2508 self.comboAudioPlayerApp.set_model(self.user_apps_reader.get_applications_as_model('audio'))
2509 index = self.find_active_audio_app()
2510 self.comboAudioPlayerApp.set_active(index)
2511 self.comboVideoPlayerApp.set_model(self.user_apps_reader.get_applications_as_model('video'))
2512 index = self.find_active_video_app()
2513 self.comboVideoPlayerApp.set_active(index)
2515 self.ipodIcon.set_from_icon_name( 'gnome-dev-ipod', gtk.ICON_SIZE_BUTTON)
2517 def update_mountpoint( self, ipod):
2518 if ipod is None or ipod.mount_point is None:
2519 self.iPodMountpoint.set_label( '')
2520 else:
2521 self.iPodMountpoint.set_label( ipod.mount_point)
2523 def on_bluetooth_select_device_clicked(self, widget):
2524 # Stupid GTK doesn't provide us with a method to directly
2525 # edit the text of a gtk.Button without "destroying" the
2526 # image on it, so we dig into the button's widget tree and
2527 # get the gtk.Image and gtk.Label and edit the label directly.
2528 alignment = self.bluetooth_select_device.get_child()
2529 hbox = alignment.get_child()
2530 (image, label) = hbox.get_children()
2532 old_text = label.get_text()
2533 label.set_text(_('Searching...'))
2534 self.bluetooth_select_device.set_sensitive(False)
2535 while gtk.events_pending():
2536 gtk.main_iteration(False)
2538 # FIXME: Make bluetooth device discovery threaded, so
2539 # the GUI doesn't freeze while we are searching for devices
2540 found = False
2541 for name, address in util.discover_bluetooth_devices():
2542 if self.show_confirmation('Use this device as your bluetooth device?', name):
2543 gl.config.bluetooth_device_name = name
2544 gl.config.bluetooth_device_address = address
2545 self.bluetooth_device_name.set_markup('<b>%s</b>'%gl.config.bluetooth_device_name)
2546 found = True
2547 break
2548 if not found:
2549 self.show_message('No more devices found', 'Scan finished')
2550 self.bluetooth_select_device.set_sensitive(True)
2551 label.set_text(old_text)
2553 def find_active_audio_app(self):
2554 model = self.comboAudioPlayerApp.get_model()
2555 iter = model.get_iter_first()
2556 index = 0
2557 while iter is not None:
2558 command = model.get_value(iter, 1)
2559 if command == self.openApp.get_text():
2560 return index
2561 iter = model.iter_next(iter)
2562 index += 1
2563 # return last item = custom command
2564 return index-1
2566 def find_active_video_app( self):
2567 model = self.comboVideoPlayerApp.get_model()
2568 iter = model.get_iter_first()
2569 index = 0
2570 while iter is not None:
2571 command = model.get_value(iter, 1)
2572 if command == self.openVideoApp.get_text():
2573 return index
2574 iter = model.iter_next(iter)
2575 index += 1
2576 # return last item = custom command
2577 return index-1
2579 def set_download_dir( self, new_download_dir, event = None):
2580 gl.downloaddir = self.chooserDownloadTo.get_filename()
2581 if gl.downloaddir != self.chooserDownloadTo.get_filename():
2582 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'))
2584 if event:
2585 event.set()
2587 def on_auto_update_feeds_toggled( self, widget, *args):
2588 self.auto_update_frequency.set_sensitive(widget.get_active())
2590 def on_display_tray_icon_toggled( self, widget, *args):
2591 self.enable_notifications.set_sensitive(widget.get_active())
2592 self.minimize_to_tray.set_sensitive(widget.get_active())
2594 def on_cbCustomSyncName_toggled( self, widget, *args):
2595 self.entryCustomSyncName.set_sensitive( widget.get_active())
2597 def on_only_sync_not_played_toggled( self, widget, *args):
2598 self.delete_episodes_marked_played.set_sensitive( widget.get_active())
2599 if not widget.get_active():
2600 self.delete_episodes_marked_played.set_active(False)
2602 def on_delete_episodes_marked_played_toggled( self, widget, *args):
2603 if widget.get_active() and self.only_sync_not_played.get_active():
2604 self.on_sync_leave.set_active(True)
2605 self.on_sync_delete.set_sensitive(not widget.get_active())
2606 self.on_sync_mark_played.set_sensitive(not widget.get_active())
2608 def on_btnCustomSyncNameHelp_clicked( self, widget):
2609 examples = [
2610 '<i>{episode.title}</i> -&gt; <b>Interview with RMS</b>',
2611 '<i>{episode.basename}</i> -&gt; <b>70908-interview-rms</b>',
2612 '<i>{episode.published}</i> -&gt; <b>20070908</b>'
2615 info = [
2616 _('You can specify a custom format string for the file names on your MP3 player here.'),
2617 _('The format string will be used to generate a file name on your device. The file extension (e.g. ".mp3") will be added automatically.'),
2618 '\n'.join( [ ' %s' % s for s in examples ])
2621 self.show_message( '\n\n'.join( info), _('Custom format strings'))
2623 def on_gPodderProperties_destroy(self, widget, *args):
2624 self.on_btnOK_clicked( widget, *args)
2626 def on_btnConfigEditor_clicked(self, widget, *args):
2627 self.on_btnOK_clicked(widget, *args)
2628 gPodderConfigEditor()
2630 def on_comboAudioPlayerApp_changed(self, widget, *args):
2631 # find out which one
2632 iter = self.comboAudioPlayerApp.get_active_iter()
2633 model = self.comboAudioPlayerApp.get_model()
2634 command = model.get_value( iter, 1)
2635 if command == '':
2636 self.openApp.set_sensitive( True)
2637 self.openApp.show()
2638 self.labelCustomCommand.show()
2639 else:
2640 self.openApp.set_text( command)
2641 self.openApp.set_sensitive( False)
2642 self.openApp.hide()
2643 self.labelCustomCommand.hide()
2645 def on_comboVideoPlayerApp_changed(self, widget, *args):
2646 # find out which one
2647 iter = self.comboVideoPlayerApp.get_active_iter()
2648 model = self.comboVideoPlayerApp.get_model()
2649 command = model.get_value(iter, 1)
2650 if command == '':
2651 self.openVideoApp.set_sensitive(True)
2652 self.openVideoApp.show()
2653 self.label115.show()
2654 else:
2655 self.openVideoApp.set_text(command)
2656 self.openVideoApp.set_sensitive(False)
2657 self.openVideoApp.hide()
2658 self.label115.hide()
2660 def on_cbEnvironmentVariables_toggled(self, widget, *args):
2661 sens = not self.cbEnvironmentVariables.get_active()
2662 self.httpProxy.set_sensitive( sens)
2663 self.ftpProxy.set_sensitive( sens)
2665 def on_comboboxDeviceType_changed(self, widget, *args):
2666 active_item = self.comboboxDeviceType.get_active()
2668 # None
2669 sync_widgets = ( self.only_sync_not_played, self.labelSyncOptions,
2670 self.imageSyncOptions, self. separatorSyncOptions,
2671 self.on_sync_mark_played, self.on_sync_delete,
2672 self.on_sync_leave, self.label_after_sync, self.delete_episodes_marked_played)
2673 for widget in sync_widgets:
2674 if active_item == 0:
2675 widget.hide_all()
2676 else:
2677 widget.show_all()
2679 # iPod
2680 ipod_widgets = (self.ipodLabel, self.btn_iPodMountpoint,
2681 self.ipod_write_gtkpod_extended)
2682 for widget in ipod_widgets:
2683 if active_item == 1:
2684 widget.show_all()
2685 else:
2686 widget.hide_all()
2688 # filesystem-based MP3 player
2689 fs_widgets = ( self.filesystemLabel, self.btn_filesystemMountpoint,
2690 self.cbChannelSubfolder, self.cbCustomSyncName,
2691 self.entryCustomSyncName, self.btnCustomSyncNameHelp )
2692 for widget in fs_widgets:
2693 if active_item == 2:
2694 widget.show_all()
2695 else:
2696 widget.hide_all()
2698 def on_btn_iPodMountpoint_clicked(self, widget, *args):
2699 fs = gtk.FileChooserDialog( title = _('Select iPod mountpoint'), action = gtk.FILE_CHOOSER_ACTION_SELECT_FOLDER)
2700 fs.add_button( gtk.STOCK_CANCEL, gtk.RESPONSE_CANCEL)
2701 fs.add_button( gtk.STOCK_OPEN, gtk.RESPONSE_OK)
2702 fs.set_current_folder(self.iPodMountpoint.get_label())
2703 if fs.run() == gtk.RESPONSE_OK:
2704 self.iPodMountpoint.set_label( fs.get_filename())
2705 fs.destroy()
2707 def on_btn_FilesystemMountpoint_clicked(self, widget, *args):
2708 fs = gtk.FileChooserDialog( title = _('Select folder for MP3 player'), action = gtk.FILE_CHOOSER_ACTION_SELECT_FOLDER)
2709 fs.add_button( gtk.STOCK_CANCEL, gtk.RESPONSE_CANCEL)
2710 fs.add_button( gtk.STOCK_OPEN, gtk.RESPONSE_OK)
2711 fs.set_current_folder(self.filesystemMountpoint.get_label())
2712 if fs.run() == gtk.RESPONSE_OK:
2713 self.filesystemMountpoint.set_label( fs.get_filename())
2714 fs.destroy()
2716 def on_btnOK_clicked(self, widget, *args):
2717 gl.config.ipod_mount = self.iPodMountpoint.get_label()
2718 gl.config.mp3_player_folder = self.filesystemMountpoint.get_label()
2720 if gl.downloaddir != self.chooserDownloadTo.get_filename():
2721 new_download_dir = self.chooserDownloadTo.get_filename()
2722 download_dir_size = util.calculate_size( gl.downloaddir)
2723 download_dir_size_string = gl.format_filesize( download_dir_size)
2724 event = Event()
2726 dlg = gtk.Dialog( _('Moving downloads folder'), self.gPodderProperties)
2727 dlg.vbox.set_spacing( 5)
2728 dlg.set_border_width( 5)
2730 label = gtk.Label()
2731 label.set_line_wrap( True)
2732 label.set_markup( _('Moving downloads from <b>%s</b> to <b>%s</b>...') % ( saxutils.escape( gl.downloaddir), saxutils.escape( new_download_dir), ))
2733 myprogressbar = gtk.ProgressBar()
2735 # put it all together
2736 dlg.vbox.pack_start( label)
2737 dlg.vbox.pack_end( myprogressbar)
2739 # switch windows
2740 dlg.show_all()
2741 self.gPodderProperties.hide_all()
2743 # hide action area and separator line
2744 dlg.action_area.hide()
2745 dlg.set_has_separator( False)
2747 args = ( new_download_dir, event, )
2749 thread = Thread( target = self.set_download_dir, args = args)
2750 thread.start()
2752 while not event.isSet():
2753 try:
2754 new_download_dir_size = util.calculate_size( new_download_dir)
2755 except:
2756 new_download_dir_size = 0
2757 if download_dir_size > 0:
2758 fract = (1.00*new_download_dir_size) / (1.00*download_dir_size)
2759 else:
2760 fract = 0.0
2761 if fract < 0.99:
2762 myprogressbar.set_text( _('%s of %s') % ( gl.format_filesize( new_download_dir_size), download_dir_size_string, ))
2763 else:
2764 myprogressbar.set_text( _('Finishing... please wait.'))
2765 myprogressbar.set_fraction(max(0.0,min(1.0,fract)))
2766 event.wait( 0.1)
2767 while gtk.events_pending():
2768 gtk.main_iteration( False)
2770 dlg.destroy()
2772 device_type = self.comboboxDeviceType.get_active()
2773 if device_type == 0:
2774 gl.config.device_type = 'none'
2775 elif device_type == 1:
2776 gl.config.device_type = 'ipod'
2777 elif device_type == 2:
2778 gl.config.device_type = 'filesystem'
2779 self.gPodderProperties.destroy()
2780 if self.callback_finished:
2781 self.callback_finished()
2784 class gPodderEpisode(GladeWidget):
2785 finger_friendly_widgets = ['episode_description', 'btnCloseWindow', 'btnDownload',
2786 'btnCancel', 'btnSaveFile', 'btnPlay', 'btn_website']
2788 def new(self):
2789 global WEB_BROWSER_ICON
2790 self.image3166.set_property('icon-name', WEB_BROWSER_ICON)
2791 services.download_status_manager.register( 'list-changed', self.on_download_status_changed)
2792 services.download_status_manager.register( 'progress-detail', self.on_download_status_progress)
2794 self.episode_title.set_markup( '<span weight="bold" size="larger">%s</span>' % saxutils.escape( self.episode.title))
2796 if gpodder.interface == gpodder.MAEMO:
2797 # Hide the advanced prefs expander
2798 self.expander1.hide_all()
2800 b = gtk.TextBuffer()
2801 b.set_text( strip( self.episode.description))
2802 self.episode_description.set_buffer( b)
2804 self.gPodderEpisode.set_title( self.episode.title)
2805 self.LabelDownloadLink.set_text( self.episode.url)
2806 self.LabelWebsiteLink.set_text( self.episode.link)
2807 self.labelPubDate.set_text(self.episode.cute_pubdate())
2809 # Hide the "Go to website" button if we don't have a valid URL
2810 if self.episode.link == self.episode.url or not self.episode.link:
2811 self.btn_website.hide_all()
2813 self.channel_title.set_markup( _('<i>from %s</i>') % saxutils.escape( self.channel.title))
2815 self.hide_show_widgets()
2816 services.download_status_manager.request_progress_detail( self.episode.url)
2818 def on_btnCancel_clicked( self, widget):
2819 services.download_status_manager.cancel_by_url( self.episode.url)
2821 def on_gPodderEpisode_destroy( self, widget):
2822 services.download_status_manager.unregister( 'list-changed', self.on_download_status_changed)
2823 services.download_status_manager.unregister( 'progress-detail', self.on_download_status_progress)
2825 def on_download_status_changed( self):
2826 self.hide_show_widgets()
2828 def on_btn_website_clicked(self, widget):
2829 util.open_website(self.episode.link)
2831 def on_download_status_progress( self, url, progress, speed):
2832 if url == self.episode.url:
2833 progress = float(min(100.0,max(0.0,progress)))
2834 self.progress_bar.set_fraction(progress/100.0)
2835 self.progress_bar.set_text( 'Downloading: %d%% (%s)' % ( progress, speed, ))
2837 def hide_show_widgets( self):
2838 is_downloading = services.download_status_manager.is_download_in_progress( self.episode.url)
2839 if is_downloading:
2840 self.progress_bar.show_all()
2841 self.btnCancel.show_all()
2842 self.btnPlay.hide_all()
2843 self.btnSaveFile.hide_all()
2844 self.btnDownload.hide_all()
2845 else:
2846 self.progress_bar.hide_all()
2847 self.btnCancel.hide_all()
2848 if os.path.exists( self.episode.local_filename()):
2849 self.btnPlay.show_all()
2850 self.btnSaveFile.show_all()
2851 self.btnDownload.hide_all()
2852 else:
2853 self.btnPlay.hide_all()
2854 self.btnSaveFile.hide_all()
2855 self.btnDownload.show_all()
2857 def on_btnCloseWindow_clicked(self, widget, *args):
2858 self.gPodderEpisode.destroy()
2860 def on_btnDownload_clicked(self, widget, *args):
2861 if self.download_callback:
2862 self.download_callback()
2864 def on_btnPlay_clicked(self, widget, *args):
2865 if self.play_callback:
2866 self.play_callback()
2868 self.gPodderEpisode.destroy()
2870 def on_btnSaveFile_clicked(self, widget, *args):
2871 self.show_copy_dialog( src_filename = self.episode.local_filename(), dst_filename = self.episode.sync_filename())
2874 class gPodderSync(GladeWidget):
2875 def new(self):
2876 util.idle_add(self.imageSync.set_from_icon_name, 'gnome-dev-ipod', gtk.ICON_SIZE_DIALOG)
2878 self.device.register('progress', self.on_progress)
2879 self.device.register('sub-progress', self.on_sub_progress)
2880 self.device.register('status', self.on_status)
2881 self.device.register('done', self.on_done)
2883 def on_progress(self, pos, max):
2884 util.idle_add(self.progressbar.set_fraction, float(pos)/float(max))
2885 util.idle_add(self.progressbar.set_text, _('%d of %d done') % (pos, max))
2887 def on_sub_progress(self, percentage):
2888 util.idle_add(self.progressbar.set_text, _('Processing (%d%%)') % (percentage))
2890 def on_status(self, status):
2891 util.idle_add(self.status_label.set_markup, '<i>%s</i>' % saxutils.escape(status))
2893 def on_done(self):
2894 util.idle_add(self.gPodderSync.destroy)
2895 if not self.gPodder.minimized:
2896 util.idle_add(self.notification, _('Your device has been updated by gPodder.'), _('Operation finished'))
2898 def on_gPodderSync_destroy(self, widget, *args):
2899 self.device.unregister('progress', self.on_progress)
2900 self.device.unregister('sub-progress', self.on_sub_progress)
2901 self.device.unregister('status', self.on_status)
2902 self.device.unregister('done', self.on_done)
2903 self.device.cancel()
2905 def on_cancel_button_clicked(self, widget, *args):
2906 self.device.cancel()
2909 class gPodderOpmlLister(GladeWidget):
2910 finger_friendly_widgets = ['btnDownloadOpml', 'btnCancel', 'btnOK', 'treeviewChannelChooser']
2912 def new(self):
2913 # initiate channels list
2914 self.channels = []
2915 self.callback_for_channel = None
2916 self.callback_finished = None
2918 if hasattr(self, 'custom_title'):
2919 self.gPodderOpmlLister.set_title(self.custom_title)
2920 if hasattr(self, 'hide_url_entry'):
2921 self.hbox25.hide_all()
2923 togglecell = gtk.CellRendererToggle()
2924 togglecell.set_property( 'activatable', True)
2925 togglecell.connect( 'toggled', self.callback_edited)
2926 togglecolumn = gtk.TreeViewColumn( '', togglecell, active=0)
2928 titlecell = gtk.CellRendererText()
2929 titlecell.set_property('ellipsize', pango.ELLIPSIZE_END)
2930 titlecolumn = gtk.TreeViewColumn(_('Podcast'), titlecell, markup=1)
2932 for itemcolumn in ( togglecolumn, titlecolumn ):
2933 self.treeviewChannelChooser.append_column( itemcolumn)
2935 def callback_edited( self, cell, path):
2936 model = self.treeviewChannelChooser.get_model()
2938 url = model[path][2]
2940 model[path][0] = not model[path][0]
2941 if model[path][0]:
2942 self.channels.append( url)
2943 else:
2944 self.channels.remove( url)
2946 self.btnOK.set_sensitive( bool(len(self.channels)))
2948 def thread_finished(self, model):
2949 self.treeviewChannelChooser.set_model(model)
2950 self.labelStatus.set_label('')
2951 self.btnDownloadOpml.set_sensitive(True)
2952 self.entryURL.set_sensitive(True)
2953 self.treeviewChannelChooser.set_sensitive(True)
2954 self.channels = []
2956 def thread_func(self):
2957 url = self.entryURL.get_text()
2958 importer = opml.Importer(url)
2959 model = importer.get_model()
2960 if len(model) == 0:
2961 self.notification(_('The specified URL does not provide any valid OPML podcast items.'), _('No feeds found'))
2962 util.idle_add(self.thread_finished, model)
2964 def get_channels_from_url( self, url, callback_for_channel = None, callback_finished = None):
2965 if callback_for_channel:
2966 self.callback_for_channel = callback_for_channel
2967 if callback_finished:
2968 self.callback_finished = callback_finished
2969 self.labelStatus.set_label( _('Downloading, please wait...'))
2970 self.entryURL.set_text( url)
2971 self.btnDownloadOpml.set_sensitive( False)
2972 self.entryURL.set_sensitive( False)
2973 self.btnOK.set_sensitive( False)
2974 self.treeviewChannelChooser.set_sensitive( False)
2975 Thread( target = self.thread_func).start()
2977 def select_all( self, value ):
2978 self.channels = []
2979 for row in self.treeviewChannelChooser.get_model():
2980 row[0] = value
2981 if value:
2982 self.channels.append(row[2])
2983 self.btnOK.set_sensitive(bool(len(self.channels)))
2985 def on_gPodderOpmlLister_destroy(self, widget, *args):
2986 pass
2988 def on_btnDownloadOpml_clicked(self, widget, *args):
2989 self.get_channels_from_url( self.entryURL.get_text())
2991 def on_btnSelectAll_clicked(self, widget, *args):
2992 self.select_all(True)
2994 def on_btnSelectNone_clicked(self, widget, *args):
2995 self.select_all(False)
2997 def on_btnOK_clicked(self, widget, *args):
2998 self.gPodderOpmlLister.destroy()
3000 # add channels that have been selected
3001 for url in self.channels:
3002 if self.callback_for_channel:
3003 self.callback_for_channel( url)
3005 if self.callback_finished:
3006 self.callback_finished()
3008 def on_btnCancel_clicked(self, widget, *args):
3009 self.gPodderOpmlLister.destroy()
3012 class gPodderEpisodeSelector( GladeWidget):
3013 """Episode selection dialog
3015 Optional keyword arguments that modify the behaviour of this dialog:
3017 - callback: Function that takes 1 parameter which is a list of
3018 the selected episodes (or empty list when none selected)
3019 - episodes: List of episodes that are presented for selection
3020 - selected: (optional) List of boolean variables that define the
3021 default checked state for the given episodes
3022 - selected_default: (optional) The default boolean value for the
3023 checked state if no other value is set
3024 (default is False)
3025 - columns: List of (name,caption) pairs for the columns, the name
3026 is the attribute name of the episode to be read from
3027 each episode object and the caption attribute is the
3028 text that appear as column caption
3029 (default is [('title','Episode'),])
3030 - title: (optional) The title of the window + heading
3031 - instructions: (optional) A one-line text describing what the
3032 user should select / what the selection is for
3033 - stock_ok_button: (optional) Will replace the "OK" button with
3034 another GTK+ stock item to be used for the
3035 affirmative button of the dialog (e.g. can
3036 be gtk.STOCK_DELETE when the episodes to be
3037 selected will be deleted after closing the
3038 dialog)
3039 - selection_buttons: (optional) A dictionary with labels as
3040 keys and callbacks as values; for each
3041 key a button will be generated, and when
3042 the button is clicked, the callback will
3043 be called for each episode and the return
3044 value of the callback (True or False) will
3045 be the new selected state of the episode
3046 - size_attribute: (optional) The name of an attribute of the
3047 supplied episode objects that can be used to
3048 calculate the size of an episode; set this to
3049 None if no total size calculation should be
3050 done (in cases where total size is useless)
3051 (default is 'length')
3054 finger_friendly_widgets = ['btnCancel', 'btnOK', 'btnCheckAll', 'btnCheckNone', 'treeviewEpisodes']
3056 COLUMN_TOGGLE = 0
3057 COLUMN_ADDITIONAL = 1
3059 def new( self):
3060 if not hasattr( self, 'callback'):
3061 self.callback = None
3063 if not hasattr( self, 'episodes'):
3064 self.episodes = []
3066 if not hasattr( self, 'size_attribute'):
3067 self.size_attribute = 'length'
3069 if not hasattr( self, 'selection_buttons'):
3070 self.selection_buttons = {}
3072 if not hasattr( self, 'selected_default'):
3073 self.selected_default = False
3075 if not hasattr( self, 'selected'):
3076 self.selected = [self.selected_default]*len(self.episodes)
3078 if len(self.selected) < len(self.episodes):
3079 self.selected += [self.selected_default]*(len(self.episodes)-len(self.selected))
3081 if not hasattr( self, 'columns'):
3082 self.columns = ( ('title', _('Episode')), )
3084 if hasattr( self, 'title'):
3085 self.gPodderEpisodeSelector.set_title( self.title)
3086 self.labelHeading.set_markup( '<b><big>%s</big></b>' % saxutils.escape( self.title))
3088 if gpodder.interface == gpodder.MAEMO:
3089 self.labelHeading.hide()
3091 if hasattr( self, 'instructions'):
3092 self.labelInstructions.set_text( self.instructions)
3093 self.labelInstructions.show_all()
3095 if hasattr(self, 'stock_ok_button'):
3096 if self.stock_ok_button == 'gpodder-download':
3097 self.btnOK.set_image(gtk.image_new_from_stock(gtk.STOCK_GO_DOWN, gtk.ICON_SIZE_BUTTON))
3098 self.btnOK.set_label(_('Download'))
3099 else:
3100 self.btnOK.set_label(self.stock_ok_button)
3101 self.btnOK.set_use_stock(True)
3103 toggle_cell = gtk.CellRendererToggle()
3104 toggle_cell.connect( 'toggled', self.toggle_cell_handler)
3106 self.treeviewEpisodes.append_column( gtk.TreeViewColumn( '', toggle_cell, active=self.COLUMN_TOGGLE))
3108 next_column = self.COLUMN_ADDITIONAL
3109 for name, caption in self.columns:
3110 renderer = gtk.CellRendererText()
3111 renderer.set_property( 'ellipsize', pango.ELLIPSIZE_END)
3112 column = gtk.TreeViewColumn( caption, renderer, text=next_column)
3113 column.set_resizable( True)
3114 # Only set "expand" on the first column (so more text is displayed there)
3115 column.set_expand(next_column == self.COLUMN_ADDITIONAL)
3116 self.treeviewEpisodes.append_column( column)
3117 next_column += 1
3119 column_types = [ gobject.TYPE_BOOLEAN ] + [ gobject.TYPE_STRING ] * len(self.columns)
3120 self.model = gtk.ListStore( *column_types)
3122 for index, episode in enumerate( self.episodes):
3123 row = [ self.selected[index] ]
3124 for name, caption in self.columns:
3125 if not hasattr(episode, name):
3126 log('Warning: Missing attribute "%s"', name, sender=self)
3127 row.append(None)
3128 else:
3129 row.append(getattr( episode, name))
3130 self.model.append( row)
3132 for label in self.selection_buttons:
3133 button = gtk.Button( label)
3134 button.connect('clicked', self.custom_selection_button_clicked, label)
3135 self.hboxButtons.pack_start( button, expand = False)
3136 button.show_all()
3138 self.treeviewEpisodes.set_rules_hint( True)
3139 self.treeviewEpisodes.set_model( self.model)
3140 self.treeviewEpisodes.columns_autosize()
3141 self.calculate_total_size()
3143 def calculate_total_size( self):
3144 if self.size_attribute is not None:
3145 (total_size, count) = (0, 0)
3146 for index, row in enumerate( self.model):
3147 if self.model.get_value( row.iter, self.COLUMN_TOGGLE) == True:
3148 try:
3149 total_size += int(getattr( self.episodes[index], self.size_attribute))
3150 count += 1
3151 except:
3152 log( 'Cannot get size for %s', self.episodes[index].title, sender = self)
3154 if total_size > 0:
3155 text = []
3156 if count == 1:
3157 text.append(_('One episodes selected'))
3158 else:
3159 text.append(_('%d episodes selected') % count)
3160 text.append(_('total size: %s') % gl.format_filesize(total_size))
3161 self.labelTotalSize.set_text(', '.join(text))
3162 self.btnOK.set_sensitive(True)
3163 else:
3164 self.labelTotalSize.set_text(_('Nothing selected'))
3165 self.btnOK.set_sensitive(False)
3166 else:
3167 self.btnOK.set_sensitive(False)
3168 for index, row in enumerate(self.model):
3169 if self.model.get_value(row.iter, self.COLUMN_TOGGLE) == True:
3170 self.btnOK.set_sensitive(True)
3171 break
3172 self.labelTotalSize.set_text('')
3174 def toggle_cell_handler( self, cell, path):
3175 model = self.treeviewEpisodes.get_model()
3176 model[path][self.COLUMN_TOGGLE] = not model[path][self.COLUMN_TOGGLE]
3178 self.calculate_total_size()
3180 def custom_selection_button_clicked(self, button, label):
3181 callback = self.selection_buttons[label]
3183 for index, row in enumerate( self.model):
3184 new_value = callback( self.episodes[index])
3185 self.model.set_value( row.iter, self.COLUMN_TOGGLE, new_value)
3187 self.calculate_total_size()
3189 def on_btnCheckAll_clicked( self, widget):
3190 for row in self.model:
3191 self.model.set_value( row.iter, self.COLUMN_TOGGLE, True)
3193 self.calculate_total_size()
3195 def on_btnCheckNone_clicked( self, widget):
3196 for row in self.model:
3197 self.model.set_value( row.iter, self.COLUMN_TOGGLE, False)
3199 self.calculate_total_size()
3201 def get_selected_episodes( self):
3202 selected_episodes = []
3204 for index, row in enumerate( self.model):
3205 if self.model.get_value( row.iter, self.COLUMN_TOGGLE) == True:
3206 selected_episodes.append( self.episodes[index])
3208 return selected_episodes
3210 def on_btnOK_clicked( self, widget):
3211 self.gPodderEpisodeSelector.destroy()
3212 if self.callback is not None:
3213 self.callback( self.get_selected_episodes())
3215 def on_btnCancel_clicked( self, widget):
3216 self.gPodderEpisodeSelector.destroy()
3217 if self.callback is not None:
3218 self.callback([])
3220 class gPodderConfigEditor(GladeWidget):
3221 finger_friendly_widgets = ['btnShowAll', 'btnClose', 'configeditor']
3223 def new(self):
3224 name_column = gtk.TreeViewColumn(_('Variable'))
3225 name_renderer = gtk.CellRendererText()
3226 name_column.pack_start(name_renderer)
3227 name_column.add_attribute(name_renderer, 'text', 0)
3228 name_column.add_attribute(name_renderer, 'weight', 5)
3229 self.configeditor.append_column(name_column)
3231 type_column = gtk.TreeViewColumn(_('Type'))
3232 type_renderer = gtk.CellRendererText()
3233 type_column.pack_start(type_renderer)
3234 type_column.add_attribute(type_renderer, 'text', 1)
3235 type_column.add_attribute(type_renderer, 'weight', 5)
3236 self.configeditor.append_column(type_column)
3238 value_column = gtk.TreeViewColumn(_('Value'))
3239 value_renderer = gtk.CellRendererText()
3240 value_column.pack_start(value_renderer)
3241 value_column.add_attribute(value_renderer, 'text', 2)
3242 value_column.add_attribute(value_renderer, 'editable', 4)
3243 value_column.add_attribute(value_renderer, 'weight', 5)
3244 value_renderer.connect('edited', self.value_edited)
3245 self.configeditor.append_column(value_column)
3247 self.model = gl.config.model()
3248 self.filter = self.model.filter_new()
3249 self.filter.set_visible_func(self.visible_func)
3251 self.configeditor.set_model(self.filter)
3252 self.configeditor.set_rules_hint(True)
3254 def visible_func(self, model, iter, user_data=None):
3255 text = self.entryFilter.get_text().lower()
3256 if text == '':
3257 return True
3258 else:
3259 # either the variable name or its value
3260 return (text in model.get_value(iter, 0).lower() or
3261 text in model.get_value(iter, 2).lower())
3263 def value_edited(self, renderer, path, new_text):
3264 model = self.configeditor.get_model()
3265 iter = model.get_iter(path)
3266 name = model.get_value(iter, 0)
3267 type_cute = model.get_value(iter, 1)
3269 if not gl.config.update_field(name, new_text):
3270 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))
3272 def on_entryFilter_changed(self, widget):
3273 self.filter.refilter()
3275 def on_btnShowAll_clicked(self, widget):
3276 self.entryFilter.set_text('')
3277 self.entryFilter.grab_focus()
3279 def on_configeditor_row_activated(self, treeview, path, view_column):
3280 model = treeview.get_model()
3281 it = model.get_iter(path)
3282 field_name = model.get_value(it, 0)
3283 field_type = model.get_value(it, 3)
3285 # Flip the boolean config flag
3286 if field_type == bool:
3287 gl.config.toggle_flag(field_name)
3289 def on_btnClose_clicked(self, widget):
3290 self.gPodderConfigEditor.destroy()
3293 def main():
3294 gobject.threads_init()
3295 gtk.window_set_default_icon_name( 'gpodder')
3297 gPodder().run()