Improve performance by minimizing commits.
[gpodder.git] / src / gpodder / gui.py
blob22951e95cfcb230971e0768e105d6bf28dc19d51
1 # -*- coding: utf-8 -*-
3 # gPodder - A media aggregator and podcast client
4 # Copyright (c) 2005-2008 Thomas Perl and the gPodder Team
6 # gPodder is free software; you can redistribute it and/or modify
7 # it under the terms of the GNU General Public License as published by
8 # the Free Software Foundation; either version 3 of the License, or
9 # (at your option) any later version.
11 # gPodder is distributed in the hope that it will be useful,
12 # but WITHOUT ANY WARRANTY; without even the implied warranty of
13 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14 # GNU General Public License for more details.
16 # You should have received a copy of the GNU General Public License
17 # along with this program. If not, see <http://www.gnu.org/licenses/>.
20 import os
21 import gtk
22 import gtk.gdk
23 import gobject
24 import pango
25 import sys
26 import shutil
27 import subprocess
28 import glob
29 import time
30 import urllib
31 import urllib2
32 import datetime
34 from xml.sax import saxutils
36 from threading import Event
37 from threading import Thread
38 from threading import Semaphore
39 from string import strip
41 import gpodder
42 from gpodder import util
43 from gpodder import opml
44 from gpodder import services
45 from gpodder import sync
46 from gpodder import download
47 from gpodder import SimpleGladeApp
48 from gpodder.liblogger import log
49 from gpodder.dbsqlite import db
51 try:
52 from gpodder import trayicon
53 have_trayicon = True
54 except Exception, exc:
55 log('Warning: Could not import gpodder.trayicon.', traceback=True)
56 log('Warning: This probably means your PyGTK installation is too old!')
57 have_trayicon = False
59 from libpodcasts import podcastChannel
60 from libpodcasts import LocalDBReader
61 from libpodcasts import podcastItem
62 from libpodcasts import channels_to_model
63 from libpodcasts import update_channel_model_by_iter
64 from libpodcasts import load_channels
65 from libpodcasts import update_channels
66 from libpodcasts import save_channels
67 from libpodcasts import can_restore_from_opml
69 from gpodder.libgpodder import gl
71 from libplayers import UserAppsReader
73 from libtagupdate import tagging_supported
75 if gpodder.interface == gpodder.GUI:
76 WEB_BROWSER_ICON = 'web-browser'
77 elif gpodder.interface == gpodder.MAEMO:
78 import hildon
79 WEB_BROWSER_ICON = 'qgn_toolb_browser_web'
81 app_name = "gpodder"
82 app_version = "unknown" # will be set in main() call
83 app_authors = [
84 _('Current maintainer:'), 'Thomas Perl <thpinfo.com>',
85 '',
86 _('Patches, bug reports and donations by:'), 'Adrien Beaucreux',
87 'Alain Tauch', 'Alistair Sutton', 'Anders Kvist', 'Andy Busch',
88 'Antonio Roversi', 'Aravind Seshadri', 'Atte André Jensen', 'audioworld',
89 'Bastian Staeck', 'Bernd Schlapsi', 'Bill Barnard', 'Bill Peters', 'Bjørn Rasmussen', 'Camille Moncelier',
90 'Carlos Moffat', 'Chris', 'Chris Arnold', 'Clark Burbidge', 'Daniel Ramos',
91 'David Spreen', 'Doug Hellmann', 'FFranci72', 'Florian Richter', 'Frank Harper',
92 'Franz Seidl', 'FriedBunny', 'Gerrit Sangel', 'Götz Waschk',
93 'Haim Roitgrund', 'Heinz Erhard', 'Hex', 'Holger Bauer', 'Holger Leskien', 'Jens Thiele',
94 'Jérôme Chabod', 'Jerry Moss',
95 'Jessica Henline', 'João Trindade', 'Joel Calado', 'John Ferguson',
96 'José Luis Fustel', 'Joseph Bleau', 'Julio Acuña', 'Junio C Hamano',
97 'Jürgen Schinker', 'Justin Forest',
98 'Konstantin Ryabitsev', 'Leonid Ponomarev', 'Marcos Hernández', 'Mark Alford', 'Markus Golser', 'Michael Salim',
99 'Mika Leppinen', 'Mike Coulson', 'Mykola Nikishov', 'narf at inode.at',
100 'Nick L.', 'Nicolas Quienot', 'Ondrej Vesely',
101 'Ortwin Forster', 'Paul Elliot', 'Paul Rudkin',
102 'Pavel Mlčoch', 'Peter Hoffmann', 'PhilF', 'Philippe Gouaillier', 'Pieter de Decker',
103 'Preben Randhol', 'Rafael Proença', 'red26wings', 'Richard Voigt',
104 'Robert Young', 'Roel Groeneveld',
105 'Scott Wegner', 'Sebastian Krause', 'Seth Remington', 'Shane Donohoe', 'Silvio Sisto', 'SPGoetze',
106 'Stefan Lohmaier', 'Stephan Buys', 'Stylianos Papanastasiou', 'Teo Ramirez',
107 'Thomas Matthijs', 'Thomas Mills Hinkle', 'Thomas Nilsson',
108 'Tim Michelsen', 'Tim Preetz', 'Todd Zullinger', 'Tomas Matheson', 'VladDrac',
109 'Vladimir Zemlyakov', 'Wilfred van Rooijen',
111 'List may be incomplete - please contact me.'
113 app_copyright = '© 2005-2008 Thomas Perl and the gPodder Team'
114 app_website = 'http://www.gpodder.org/'
116 # these will be filled with pathnames in bin/gpodder
117 glade_dir = [ 'share', 'gpodder' ]
118 icon_dir = [ 'share', 'pixmaps', 'gpodder.png' ]
119 scalable_dir = [ 'share', 'icons', 'hicolor', 'scalable', 'apps', 'gpodder.svg' ]
122 class GladeWidget(SimpleGladeApp.SimpleGladeApp):
123 gpodder_main_window = None
124 finger_friendly_widgets = []
126 def __init__( self, **kwargs):
127 path = os.path.join( glade_dir, '%s.glade' % app_name)
128 root = self.__class__.__name__
129 domain = app_name
131 SimpleGladeApp.SimpleGladeApp.__init__( self, path, root, domain, **kwargs)
133 # Set widgets to finger-friendly mode if on Maemo
134 for widget_name in self.finger_friendly_widgets:
135 self.set_finger_friendly(getattr(self, widget_name))
137 if root == 'gPodder':
138 GladeWidget.gpodder_main_window = self.gPodder
139 else:
140 # If we have a child window, set it transient for our main window
141 getattr( self, root).set_transient_for( GladeWidget.gpodder_main_window)
143 if gpodder.interface == gpodder.GUI:
144 if hasattr( self, 'center_on_widget'):
145 ( x, y ) = self.gpodder_main_window.get_position()
146 a = self.center_on_widget.allocation
147 ( x, y ) = ( x + a.x, y + a.y )
148 ( w, h ) = ( a.width, a.height )
149 ( pw, ph ) = getattr( self, root).get_size()
150 getattr( self, root).move( x + w/2 - pw/2, y + h/2 - ph/2)
151 else:
152 getattr( self, root).set_position( gtk.WIN_POS_CENTER_ON_PARENT)
154 def notification(self, message, title=None):
155 util.idle_add(self.show_message, message, title)
157 def show_message( self, message, title = None):
158 if hasattr(self, 'tray_icon') and hasattr(self, 'minimized') and self.tray_icon and self.minimized:
159 if title is None:
160 title = 'gPodder'
161 self.tray_icon.send_notification(message, title)
162 return
164 if gpodder.interface == gpodder.GUI:
165 dlg = gtk.MessageDialog(GladeWidget.gpodder_main_window, gtk.DIALOG_MODAL, gtk.MESSAGE_INFO, gtk.BUTTONS_OK)
166 if title:
167 dlg.set_title(str(title))
168 dlg.set_markup('<span weight="bold" size="larger">%s</span>\n\n%s' % (title, message))
169 else:
170 dlg.set_markup('<span weight="bold" size="larger">%s</span>' % (message))
171 elif gpodder.interface == gpodder.MAEMO:
172 dlg = hildon.Note('information', (GladeWidget.gpodder_main_window, message))
174 dlg.run()
175 dlg.destroy()
177 def set_finger_friendly(self, widget):
179 If we are on Maemo, we carry out the necessary
180 operations to turn a widget into a finger-friendly
181 one, depending on which type of widget it is (i.e.
182 buttons will have more padding, TreeViews a thick
183 scrollbar, etc..)
185 if gpodder.interface == gpodder.MAEMO:
186 if isinstance(widget, gtk.Misc):
187 widget.set_padding(0, 5)
188 elif isinstance(widget, gtk.Button):
189 for child in widget.get_children():
190 if isinstance(child, gtk.Alignment):
191 child.set_padding(10, 10, 5, 5)
192 else:
193 child.set_padding(10, 10)
194 elif isinstance(widget, gtk.TreeView) or isinstance(widget, gtk.TextView):
195 parent = widget.get_parent()
196 if isinstance(parent, gtk.ScrolledWindow):
197 hildon.hildon_helper_set_thumb_scrollbar(parent, True)
198 elif isinstance(widget, gtk.MenuItem):
199 for child in widget.get_children():
200 self.set_finger_friendly(child)
201 else:
202 log('Cannot set widget finger-friendly: %s', widget, sender=self)
204 return widget
206 def show_confirmation( self, message, title = None):
207 if gpodder.interface == gpodder.GUI:
208 affirmative = gtk.RESPONSE_YES
209 dlg = gtk.MessageDialog(GladeWidget.gpodder_main_window, gtk.DIALOG_MODAL, gtk.MESSAGE_QUESTION, gtk.BUTTONS_YES_NO)
210 if title:
211 dlg.set_title(str(title))
212 dlg.set_markup('<span weight="bold" size="larger">%s</span>\n\n%s' % (title, message))
213 else:
214 dlg.set_markup('<span weight="bold" size="larger">%s</span>' % (message))
215 elif gpodder.interface == gpodder.MAEMO:
216 affirmative = gtk.RESPONSE_OK
217 dlg = hildon.Note('confirmation', (GladeWidget.gpodder_main_window, message))
219 response = dlg.run()
220 dlg.destroy()
222 return response == affirmative
224 def show_copy_dialog( self, src_filename, dst_filename = None, dst_directory = None, title = _('Select destination')):
225 if dst_filename is None:
226 dst_filename = src_filename
228 if dst_directory is None:
229 dst_directory = os.path.expanduser( '~')
231 ( base, extension ) = os.path.splitext( src_filename)
233 if not dst_filename.endswith( extension):
234 dst_filename += extension
236 if gpodder.interface == gpodder.GUI:
237 dlg = gtk.FileChooserDialog(title=title, parent=GladeWidget.gpodder_main_window, action=gtk.FILE_CHOOSER_ACTION_SAVE)
238 dlg.add_button(gtk.STOCK_CANCEL, gtk.RESPONSE_CANCEL)
239 dlg.add_button(gtk.STOCK_SAVE, gtk.RESPONSE_OK)
240 elif gpodder.interface == gpodder.MAEMO:
241 dlg = hildon.FileChooserDialog(GladeWidget.gpodder_main_window, gtk.FILE_CHOOSER_ACTION_SAVE)
243 dlg.set_do_overwrite_confirmation( True)
244 dlg.set_current_name( os.path.basename( dst_filename))
245 dlg.set_current_folder( dst_directory)
247 if dlg.run() == gtk.RESPONSE_OK:
248 dst_filename = dlg.get_filename()
249 if not dst_filename.endswith( extension):
250 dst_filename += extension
252 log( 'Copying %s => %s', src_filename, dst_filename, sender = self)
254 try:
255 shutil.copyfile( src_filename, dst_filename)
256 except:
257 log( 'Error copying file.', sender = self, traceback = True)
259 dlg.destroy()
263 class gPodder(GladeWidget):
264 finger_friendly_widgets = ['btnUpdateFeeds', 'btnCancelFeedUpdate', 'treeAvailable', 'label2', 'labelDownloads']
265 ENTER_URL_TEXT = _('Enter podcast URL...')
267 def new(self):
268 if gpodder.interface == gpodder.MAEMO:
269 # Maemo-specific changes to the UI
270 global scalable_dir
271 scalable_dir = scalable_dir.replace('.svg', '.png')
273 self.app = hildon.Program()
274 gtk.set_application_name('gPodder')
275 self.window = hildon.Window()
276 self.window.connect('delete-event', self.on_gPodder_delete_event)
277 self.window.connect('window-state-event', self.window_state_event)
279 self.itemUpdateChannel.show()
280 self.UpdateChannelSeparator.show()
282 # Give toolbar to the hildon window
283 self.toolbar.parent.remove(self.toolbar)
284 self.toolbar.set_style(gtk.TOOLBAR_ICONS)
285 self.window.add_toolbar(self.toolbar)
287 self.app.add_window(self.window)
288 self.vMain.reparent(self.window)
289 self.gPodder = self.window
291 # Reparent the main menu
292 menu = gtk.Menu()
293 for child in self.mainMenu.get_children():
294 child.reparent(menu)
295 self.itemQuit.reparent(menu)
296 self.window.set_menu(menu)
298 self.mainMenu.destroy()
299 self.window.show()
301 # do some widget hiding
302 self.toolbar.remove(self.toolTransfer)
303 self.itemTransferSelected.hide_all()
304 self.item_show_url_entry.hide_all()
305 self.item_email_subscriptions.hide_all()
307 # Feed cache update button
308 self.label120.set_text(_('Update'))
310 # get screen real estate
311 self.hboxContainer.set_border_width(0)
313 self.gPodder.connect('key-press-event', self.on_key_press)
314 self.treeChannels.connect('size-allocate', self.on_tree_channels_resize)
316 if gpodder.interface == gpodder.MAEMO or not gl.config.show_podcast_url_entry:
317 self.hboxAddChannel.hide_all()
319 if not gl.config.show_toolbar:
320 self.toolbar.hide_all()
322 gl.config.add_observer(self.on_config_changed)
323 self.default_entry_text_color = self.entryAddChannel.get_style().text[gtk.STATE_NORMAL]
324 self.entryAddChannel.connect('focus-in-event', self.entry_add_channel_focus)
325 self.entryAddChannel.connect('focus-out-event', self.entry_add_channel_unfocus)
326 self.entry_add_channel_unfocus(self.entryAddChannel, None)
328 self.uar = None
329 self.tray_icon = None
331 self.fullscreen = False
332 self.minimized = False
333 self.gPodder.connect('window-state-event', self.window_state_event)
335 self.already_notified_new_episodes = []
336 self.show_hide_tray_icon()
337 self.no_episode_selected.set_sensitive(False)
339 self.itemShowToolbar.set_active(gl.config.show_toolbar)
340 self.itemShowDescription.set_active(gl.config.episode_list_descriptions)
341 self.item_show_url_entry.set_active(gl.config.show_podcast_url_entry)
343 gl.config.connect_gtk_window( self.gPodder)
344 gl.config.connect_gtk_paned( 'paned_position', self.channelPaned)
346 gl.config.connect_gtk_spinbutton('max_downloads', self.spinMaxDownloads)
347 gl.config.connect_gtk_togglebutton('max_downloads_enabled', self.cbMaxDownloads)
348 gl.config.connect_gtk_spinbutton('limit_rate_value', self.spinLimitDownloads)
349 gl.config.connect_gtk_togglebutton('limit_rate', self.cbLimitDownloads)
351 # Make sure we free/close the download queue when we
352 # update the "max downloads" spin button
353 changed_cb = lambda spinbutton: services.download_status_manager.update_max_downloads()
354 self.spinMaxDownloads.connect('value-changed', changed_cb)
356 self.default_title = None
357 if app_version.rfind('git') != -1:
358 self.set_title('gPodder %s' % app_version)
359 else:
360 title = self.gPodder.get_title()
361 if title is not None:
362 self.set_title(title)
363 else:
364 self.set_title(_('gPodder'))
366 gtk.about_dialog_set_url_hook(lambda dlg, link, data: util.open_website(link), None)
368 # cell renderers for channel tree
369 namecolumn = gtk.TreeViewColumn( _('Podcast'))
371 iconcell = gtk.CellRendererPixbuf()
372 namecolumn.pack_start( iconcell, False)
373 namecolumn.add_attribute( iconcell, 'pixbuf', 5)
374 self.cell_channel_icon = iconcell
376 namecell = gtk.CellRendererText()
377 namecell.set_property('foreground-set', True)
378 namecell.set_property('ellipsize', pango.ELLIPSIZE_END)
379 namecolumn.pack_start( namecell, True)
380 namecolumn.add_attribute( namecell, 'markup', 2)
381 namecolumn.add_attribute( namecell, 'foreground', 8)
383 iconcell = gtk.CellRendererPixbuf()
384 iconcell.set_property('xalign', 1.0)
385 namecolumn.pack_start( iconcell, False)
386 namecolumn.add_attribute( iconcell, 'pixbuf', 3)
387 namecolumn.add_attribute(iconcell, 'visible', 7)
388 self.cell_channel_pill = iconcell
390 self.treeChannels.append_column( namecolumn)
391 self.treeChannels.set_headers_visible(False)
393 # enable alternating colors hint
394 self.treeAvailable.set_rules_hint( True)
395 self.treeChannels.set_rules_hint( True)
397 # connect to tooltip signals
398 try:
399 self.treeChannels.set_property('has-tooltip', True)
400 self.treeChannels.connect('query-tooltip', self.treeview_channels_query_tooltip)
401 self.treeAvailable.set_property('has-tooltip', True)
402 self.treeAvailable.connect('query-tooltip', self.treeview_episodes_query_tooltip)
403 except:
404 log('I cannot set has-tooltip/query-tooltip (need at least PyGTK 2.12)', sender = self)
405 self.last_tooltip_channel = None
406 self.last_tooltip_episode = None
407 self.podcast_list_can_tooltip = True
408 self.episode_list_can_tooltip = True
410 # Add our context menu to treeAvailable
411 if gpodder.interface == gpodder.MAEMO:
412 self.treeAvailable.connect('button-release-event', self.treeview_button_pressed)
413 else:
414 self.treeAvailable.connect('button-press-event', self.treeview_button_pressed)
415 self.treeChannels.connect('button-press-event', self.treeview_channels_button_pressed)
417 iconcell = gtk.CellRendererPixbuf()
418 if gpodder.interface == gpodder.MAEMO:
419 status_column_label = ''
420 else:
421 status_column_label = _('Status')
422 iconcolumn = gtk.TreeViewColumn(status_column_label, iconcell, pixbuf=4)
424 namecell = gtk.CellRendererText()
425 namecell.set_property('ellipsize', pango.ELLIPSIZE_END)
426 namecolumn = gtk.TreeViewColumn(_("Episode"), namecell, markup=6)
427 namecolumn.set_sizing(gtk.TREE_VIEW_COLUMN_AUTOSIZE)
428 namecolumn.set_expand(True)
430 sizecell = gtk.CellRendererText()
431 sizecolumn = gtk.TreeViewColumn( _("Size"), sizecell, text=2)
433 releasecell = gtk.CellRendererText()
434 releasecolumn = gtk.TreeViewColumn( _("Released"), releasecell, text=5)
436 for itemcolumn in (iconcolumn, namecolumn, sizecolumn, releasecolumn):
437 itemcolumn.set_reorderable(True)
438 self.treeAvailable.append_column(itemcolumn)
440 if gpodder.interface == gpodder.MAEMO:
441 # Due to screen space contraints, we
442 # hide these columns here by default
443 self.column_size = sizecolumn
444 self.column_released = releasecolumn
445 self.column_released.set_visible(False)
446 self.column_size.set_visible(False)
448 # enable search in treeavailable
449 self.treeAvailable.set_search_equal_func( self.treeAvailable_search_equal)
451 # enable multiple selection support
452 self.treeAvailable.get_selection().set_mode( gtk.SELECTION_MULTIPLE)
453 self.treeDownloads.get_selection().set_mode( gtk.SELECTION_MULTIPLE)
455 # columns and renderers for "download progress" tab
456 episodecell = gtk.CellRendererText()
457 episodecell.set_property('ellipsize', pango.ELLIPSIZE_END)
458 episodecolumn = gtk.TreeViewColumn( _("Episode"), episodecell, text=0)
459 episodecolumn.set_sizing(gtk.TREE_VIEW_COLUMN_AUTOSIZE)
460 episodecolumn.set_expand(True)
462 speedcell = gtk.CellRendererText()
463 speedcolumn = gtk.TreeViewColumn( _("Speed"), speedcell, text=1)
465 progresscell = gtk.CellRendererProgress()
466 progresscolumn = gtk.TreeViewColumn( _("Progress"), progresscell, value=2)
467 progresscolumn.set_expand(True)
469 for itemcolumn in ( episodecolumn, speedcolumn, progresscolumn ):
470 self.treeDownloads.append_column( itemcolumn)
472 # After we've set up most of the window, show it :)
473 if not gpodder.interface == gpodder.MAEMO:
474 self.gPodder.show()
476 if self.tray_icon:
477 if gl.config.start_iconified:
478 self.iconify_main_window()
479 elif gl.config.minimize_to_tray:
480 self.tray_icon.set_visible(False)
482 services.download_status_manager.register( 'list-changed', self.download_status_updated)
483 services.download_status_manager.register( 'progress-changed', self.download_progress_updated)
484 services.cover_downloader.register('cover-available', self.cover_download_finished)
485 services.cover_downloader.register('cover-removed', self.cover_file_removed)
486 self.cover_cache = {}
488 self.treeDownloads.set_model( services.download_status_manager.tree_model)
490 #Add Drag and Drop Support
491 flags = gtk.DEST_DEFAULT_ALL
492 targets = [ ('text/plain', 0, 2), ('STRING', 0, 3), ('TEXT', 0, 4) ]
493 actions = gtk.gdk.ACTION_DEFAULT | gtk.gdk.ACTION_COPY
494 self.treeChannels.drag_dest_set( flags, targets, actions)
495 self.treeChannels.connect( 'drag_data_received', self.drag_data_received)
497 # Subscribed channels
498 self.active_channel = None
499 self.channels = load_channels()
500 self.update_podcasts_tab()
502 # load list of user applications for audio playback
503 self.user_apps_reader = UserAppsReader(['audio', 'video'])
504 Thread(target=self.read_apps).start()
506 # Clean up old, orphaned download files
507 gl.clean_up_downloads( delete_partial = True)
509 # Set the "Device" menu item for the first time
510 self.update_item_device()
512 # Set up default channel colors
513 self.channel_colors = {
514 'default': None,
515 'updating': gl.config.color_updating_feeds,
516 'parse_error': '#ff0000',
519 # Now, update the feed cache, when everything's in place
520 self.btnUpdateFeeds.show_all()
521 self.updated_feeds = 0
522 self.updating_feed_cache = False
523 self.feed_cache_update_cancelled = False
524 self.update_feed_cache(force_update=gl.config.update_on_startup)
526 # Start the auto-update procedure
527 self.auto_update_procedure(first_run=True)
529 # Delete old episodes if the user wishes to
530 if gl.config.auto_remove_old_episodes:
531 old_episodes = self.get_old_episodes()
532 if len(old_episodes) > 0:
533 self.delete_episode_list(old_episodes, confirm=False)
534 self.updateComboBox()
536 # First-time users should be asked if they want to see the OPML
537 if len(self.channels) == 0:
538 util.idle_add(self.on_itemUpdate_activate, None)
540 def on_tree_channels_resize(self, widget, allocation):
541 if not gl.config.podcast_sidebar_save_space:
542 return
544 window_allocation = self.gPodder.get_allocation()
545 percentage = 100. * float(allocation.width) / float(window_allocation.width)
546 if hasattr(self, 'cell_channel_icon'):
547 self.cell_channel_icon.set_property('visible', bool(percentage > 22.))
548 if hasattr(self, 'cell_channel_pill'):
549 self.cell_channel_pill.set_property('visible', bool(percentage > 25.))
551 def entry_add_channel_focus(self, widget, event):
552 widget.modify_text(gtk.STATE_NORMAL, self.default_entry_text_color)
553 if widget.get_text() == self.ENTER_URL_TEXT:
554 widget.set_text('')
556 def entry_add_channel_unfocus(self, widget, event):
557 if widget.get_text() == '':
558 widget.set_text(self.ENTER_URL_TEXT)
559 widget.modify_text(gtk.STATE_NORMAL, gtk.gdk.color_parse('#aaaaaa'))
561 def on_config_changed(self, name, old_value, new_value):
562 if name == 'show_toolbar':
563 if new_value:
564 self.toolbar.show_all()
565 else:
566 self.toolbar.hide_all()
567 elif name == 'episode_list_descriptions':
568 self.updateTreeView()
569 elif name == 'show_podcast_url_entry' and gpodder.interface != gpodder.MAEMO:
570 if new_value:
571 self.hboxAddChannel.show_all()
572 else:
573 self.hboxAddChannel.hide_all()
575 def read_apps(self):
576 time.sleep(3) # give other parts of gpodder a chance to start up
577 self.user_apps_reader.read()
578 util.idle_add(self.user_apps_reader.get_applications_as_model, 'audio', False)
579 util.idle_add(self.user_apps_reader.get_applications_as_model, 'video', False)
581 def treeview_episodes_query_tooltip(self, treeview, x, y, keyboard_tooltip, tooltip):
582 # With get_bin_window, we get the window that contains the rows without
583 # the header. The Y coordinate of this window will be the height of the
584 # treeview header. This is the amount we have to subtract from the
585 # event's Y coordinate to get the coordinate to pass to get_path_at_pos
586 (x_bin, y_bin) = treeview.get_bin_window().get_position()
587 y -= x_bin
588 y -= y_bin
589 (path, column, rx, ry) = treeview.get_path_at_pos( x, y) or (None,)*4
591 if not self.episode_list_can_tooltip:
592 self.last_tooltip_episode = None
593 return False
595 if path is not None:
596 model = treeview.get_model()
597 iter = model.get_iter(path)
598 url = model.get_value(iter, 0)
599 description = model.get_value(iter, 7)
600 if self.last_tooltip_episode is not None and self.last_tooltip_episode != url:
601 self.last_tooltip_episode = None
602 return False
603 self.last_tooltip_episode = url
605 tooltip.set_text(description)
606 return True
608 self.last_tooltip_episode = None
609 return False
611 def podcast_list_allow_tooltips(self):
612 self.podcast_list_can_tooltip = True
614 def episode_list_allow_tooltips(self):
615 self.episode_list_can_tooltip = True
617 def treeview_channels_query_tooltip(self, treeview, x, y, keyboard_tooltip, tooltip):
618 (path, column, rx, ry) = treeview.get_path_at_pos( x, y) or (None,)*4
620 if not self.podcast_list_can_tooltip:
621 self.last_tooltip_channel = None
622 return False
624 if path is not None:
625 model = treeview.get_model()
626 iter = model.get_iter(path)
627 url = model.get_value(iter, 0)
628 for channel in self.channels:
629 if channel.url == url:
630 if self.last_tooltip_channel is not None and self.last_tooltip_channel != channel:
631 self.last_tooltip_channel = None
632 return False
633 self.last_tooltip_channel = channel
634 channel.request_save_dir_size()
635 diskspace_str = gl.format_filesize(channel.save_dir_size, 0)
636 error_str = model.get_value(iter, 6)
637 if error_str:
638 error_str = _('Feedparser error: %s') % saxutils.escape(error_str.strip())
639 error_str = '<span foreground="#ff0000">%s</span>' % error_str
640 table = gtk.Table(rows=3, columns=3)
641 table.set_row_spacings(5)
642 table.set_col_spacings(5)
643 table.set_border_width(5)
645 heading = gtk.Label()
646 heading.set_alignment(0, 1)
647 heading.set_markup('<b><big>%s</big></b>\n<small>%s</small>' % (saxutils.escape(channel.title), saxutils.escape(channel.url)))
648 table.attach(heading, 0, 1, 0, 1)
649 size_info = gtk.Label()
650 size_info.set_alignment(1, 1)
651 size_info.set_justify(gtk.JUSTIFY_RIGHT)
652 size_info.set_markup('<b>%s</b>\n<small>%s</small>' % (diskspace_str, _('disk usage')))
653 table.attach(size_info, 2, 3, 0, 1)
655 table.attach(gtk.HSeparator(), 0, 3, 1, 2)
657 if len(channel.description) < 500:
658 description = channel.description
659 else:
660 pos = channel.description.find('\n\n')
661 if pos == -1 or pos > 500:
662 description = channel.description[:498]+'[...]'
663 else:
664 description = channel.description[:pos]
666 description = gtk.Label(description)
667 if error_str:
668 description.set_markup(error_str)
669 description.set_alignment(0, 0)
670 description.set_line_wrap(True)
671 table.attach(description, 0, 3, 2, 3)
673 table.show_all()
674 tooltip.set_custom(table)
676 return True
678 self.last_tooltip_channel = None
679 return False
681 def update_m3u_playlist_clicked(self, widget):
682 self.active_channel.update_m3u_playlist()
683 self.show_message(_('Updated M3U playlist in download folder.'), _('Updated playlist'))
685 def treeview_channels_button_pressed( self, treeview, event):
686 global WEB_BROWSER_ICON
688 if event.button == 3:
689 ( x, y ) = ( int(event.x), int(event.y) )
690 ( path, column, rx, ry ) = treeview.get_path_at_pos( x, y) or (None,)*4
692 paths = []
694 # Did the user right-click into a selection?
695 selection = treeview.get_selection()
696 if selection.count_selected_rows() and path:
697 ( model, paths ) = selection.get_selected_rows()
698 if path not in paths:
699 # We have right-clicked, but not into the
700 # selection, assume we don't want to operate
701 # on the selection
702 paths = []
704 # No selection or right click not in selection:
705 # Select the single item where we clicked
706 if not len( paths) and path:
707 treeview.grab_focus()
708 treeview.set_cursor( path, column, 0)
710 ( model, paths ) = ( treeview.get_model(), [ path ] )
712 # We did not find a selection, and the user didn't
713 # click on an item to select -- don't show the menu
714 if not len( paths):
715 return True
717 menu = gtk.Menu()
719 item = gtk.ImageMenuItem( _('Open download folder'))
720 item.set_image( gtk.image_new_from_icon_name( 'folder-open', gtk.ICON_SIZE_MENU))
721 item.connect('activate', lambda x: util.gui_open(self.active_channel.save_dir))
722 menu.append( item)
724 item = gtk.ImageMenuItem( _('Update Feed'))
725 item.set_image( gtk.image_new_from_icon_name( 'gtk-refresh', gtk.ICON_SIZE_MENU))
726 item.connect('activate', self.on_itemUpdateChannel_activate )
727 item.set_sensitive( not self.updating_feed_cache )
728 menu.append( item)
730 if gl.config.create_m3u_playlists:
731 item = gtk.ImageMenuItem(_('Update M3U playlist'))
732 item.set_image(gtk.image_new_from_stock(gtk.STOCK_REFRESH, gtk.ICON_SIZE_MENU))
733 item.connect('activate', self.update_m3u_playlist_clicked)
734 menu.append(item)
736 if self.active_channel.link:
737 item = gtk.ImageMenuItem(_('Visit website'))
738 item.set_image(gtk.image_new_from_icon_name(WEB_BROWSER_ICON, gtk.ICON_SIZE_MENU))
739 item.connect('activate', lambda w: util.open_website(self.active_channel.link))
740 menu.append(item)
742 menu.append( gtk.SeparatorMenuItem())
744 item = gtk.ImageMenuItem(gtk.STOCK_EDIT)
745 item.connect( 'activate', self.on_itemEditChannel_activate)
746 menu.append( item)
748 item = gtk.ImageMenuItem(gtk.STOCK_DELETE)
749 item.connect( 'activate', self.on_itemRemoveChannel_activate)
750 menu.append( item)
752 menu.show_all()
753 # Disable tooltips while we are showing the menu, so
754 # the tooltip will not appear over the menu
755 self.podcast_list_can_tooltip = False
756 menu.connect('deactivate', lambda menushell: self.podcast_list_allow_tooltips())
757 menu.popup( None, None, None, event.button, event.time)
759 return True
761 def on_itemClose_activate(self, widget):
762 if self.tray_icon is not None:
763 if gpodder.interface == gpodder.MAEMO:
764 self.gPodder.set_property('visible', False)
765 else:
766 self.iconify_main_window()
767 else:
768 self.on_gPodder_delete_event(widget)
770 def cover_file_removed(self, channel_url):
772 The Cover Downloader calls this when a previously-
773 available cover has been removed from the disk. We
774 have to update our cache to reflect this change.
776 (COLUMN_URL, COLUMN_PIXBUF) = (0, 5)
777 for row in self.treeChannels.get_model():
778 if row[COLUMN_URL] == channel_url:
779 row[COLUMN_PIXBUF] = None
780 key = (channel_url, gl.config.podcast_list_icon_size, \
781 gl.config.podcast_list_icon_size)
782 if key in self.cover_cache:
783 del self.cover_cache[key]
786 def cover_download_finished(self, channel_url, pixbuf):
788 The Cover Downloader calls this when it has finished
789 downloading (or registering, if already downloaded)
790 a new channel cover, which is ready for displaying.
792 if pixbuf is not None:
793 (COLUMN_URL, COLUMN_PIXBUF) = (0, 5)
794 for row in self.treeChannels.get_model():
795 if row[COLUMN_URL] == channel_url and row[COLUMN_PIXBUF] is None:
796 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)
797 row[COLUMN_PIXBUF] = new_pixbuf or pixbuf
799 def save_episode_as_file( self, url, *args):
800 episode = self.active_channel.find_episode( url)
802 self.show_copy_dialog( src_filename = episode.local_filename(), dst_filename = episode.sync_filename())
804 def copy_episode_bluetooth(self, url, *args):
805 episode = self.active_channel.find_episode(url)
806 filename = episode.local_filename()
808 if gl.config.bluetooth_ask_always:
809 device = None
810 else:
811 device = gl.config.bluetooth_device_address
813 destfile = os.path.join(gl.tempdir, util.sanitize_filename(episode.sync_filename()))
814 (base, ext) = os.path.splitext(filename)
815 if not destfile.endswith(ext):
816 destfile += ext
818 if gl.config.bluetooth_use_converter:
819 title = _('Converting file')
820 message = _('Please wait while gPodder converts your media file for bluetooth file transfer.')
821 dlg = gtk.MessageDialog(self.gPodder, gtk.DIALOG_MODAL, gtk.MESSAGE_INFO, gtk.BUTTONS_NONE)
822 dlg.set_title(title)
823 dlg.set_markup('<span weight="bold" size="larger">%s</span>\n\n%s'%(title, message))
824 dlg.show_all()
825 else:
826 dlg = None
828 def convert_and_send_thread(filename, destfile, device, dialog, notify):
829 if gl.config.bluetooth_use_converter:
830 p = subprocess.Popen([gl.config.bluetooth_converter, filename, destfile], stdout=sys.stdout, stderr=sys.stderr)
831 result = p.wait()
832 if dialog is not None:
833 dialog.destroy()
834 else:
835 try:
836 shutil.copyfile(filename, destfile)
837 result = 0
838 except:
839 log('Cannot copy "%s" to "%s".', filename, destfile, sender=self)
840 result = 1
842 if result == 0 or not os.path.exists(destfile):
843 util.bluetooth_send_file(destfile, device)
844 else:
845 notify(_('Error converting file.'), _('Bluetooth file transfer'))
846 util.delete_file(destfile)
848 Thread(target=convert_and_send_thread, args=[filename, destfile, device, dlg, self.notification]).start()
850 def treeview_button_pressed( self, treeview, event):
851 global WEB_BROWSER_ICON
853 # Use right-click for the Desktop version and left-click for Maemo
854 if (event.button == 1 and gpodder.interface == gpodder.MAEMO) or \
855 (event.button == 3 and gpodder.interface == gpodder.GUI):
856 ( x, y ) = ( int(event.x), int(event.y) )
857 ( path, column, rx, ry ) = treeview.get_path_at_pos( x, y) or (None,)*4
859 paths = []
861 # Did the user right-click into a selection?
862 selection = self.treeAvailable.get_selection()
863 if selection.count_selected_rows() and path:
864 ( model, paths ) = selection.get_selected_rows()
865 if path not in paths:
866 # We have right-clicked, but not into the
867 # selection, assume we don't want to operate
868 # on the selection
869 paths = []
871 # No selection or right click not in selection:
872 # Select the single item where we clicked
873 if not len( paths) and path:
874 treeview.grab_focus()
875 treeview.set_cursor( path, column, 0)
877 ( model, paths ) = ( treeview.get_model(), [ path ] )
879 # We did not find a selection, and the user didn't
880 # click on an item to select -- don't show the menu
881 if not len( paths):
882 return True
884 first_url = model.get_value( model.get_iter( paths[0]), 0)
885 episode = db.load_episode(first_url)
887 menu = gtk.Menu()
889 (can_play, can_download, can_transfer, can_cancel, can_delete, open_instead_of_play) = self.play_or_download()
891 if can_play:
892 if open_instead_of_play:
893 item = gtk.ImageMenuItem(gtk.STOCK_OPEN)
894 else:
895 item = gtk.ImageMenuItem(gtk.STOCK_MEDIA_PLAY)
896 item.connect( 'activate', lambda w: self.on_treeAvailable_row_activated( self.toolPlay))
897 menu.append(self.set_finger_friendly(item))
899 if not episode['is_locked'] and can_delete:
900 item = gtk.ImageMenuItem(gtk.STOCK_DELETE)
901 item.connect('activate', self.on_btnDownloadedDelete_clicked)
902 menu.append(self.set_finger_friendly(item))
904 if can_cancel:
905 item = gtk.ImageMenuItem( _('Cancel download'))
906 item.set_image( gtk.image_new_from_stock( gtk.STOCK_STOP, gtk.ICON_SIZE_MENU))
907 item.connect( 'activate', lambda w: self.on_treeDownloads_row_activated( self.toolCancel))
908 menu.append(self.set_finger_friendly(item))
910 if can_download:
911 item = gtk.ImageMenuItem(_('Download'))
912 item.set_image( gtk.image_new_from_stock( gtk.STOCK_GO_DOWN, gtk.ICON_SIZE_MENU))
913 item.connect( 'activate', lambda w: self.on_treeAvailable_row_activated( self.toolDownload))
914 menu.append(self.set_finger_friendly(item))
916 if episode['state'] == db.STATE_NORMAL and not episode['is_played']: # can_download:
917 item = gtk.ImageMenuItem(_('Do not download'))
918 item.set_image(gtk.image_new_from_stock(gtk.STOCK_DELETE, gtk.ICON_SIZE_MENU))
919 item.connect('activate', lambda w: self.mark_selected_episodes_old())
920 menu.append(self.set_finger_friendly(item))
921 elif episode['state'] != db.STATE_NORMAL and can_download:
922 item = gtk.ImageMenuItem(_('Mark as new'))
923 item.set_image(gtk.image_new_from_stock(gtk.STOCK_ABOUT, gtk.ICON_SIZE_MENU))
924 item.connect('activate', lambda w: self.mark_selected_episodes_new())
925 menu.append(self.set_finger_friendly(item))
927 if can_play:
928 menu.append( gtk.SeparatorMenuItem())
929 item = gtk.ImageMenuItem(_('Save to disk'))
930 item.set_image(gtk.image_new_from_stock(gtk.STOCK_SAVE_AS, gtk.ICON_SIZE_MENU))
931 item.connect( 'activate', lambda w: self.for_each_selected_episode_url(self.save_episode_as_file))
932 menu.append(self.set_finger_friendly(item))
933 if gl.config.bluetooth_enabled:
934 item = gtk.ImageMenuItem(_('Send via bluetooth'))
935 item.set_image(gtk.image_new_from_icon_name('bluetooth', gtk.ICON_SIZE_MENU))
936 item.connect('activate', lambda w: self.copy_episode_bluetooth(episode_url))
937 menu.append(self.set_finger_friendly(item))
938 if can_transfer:
939 item = gtk.ImageMenuItem(_('Transfer to %s') % gl.get_device_name())
940 item.set_image(gtk.image_new_from_icon_name('multimedia-player', gtk.ICON_SIZE_MENU))
941 item.connect('activate', lambda w: self.on_treeAvailable_row_activated(self.toolTransfer))
942 menu.append(self.set_finger_friendly(item))
944 if can_play:
945 menu.append( gtk.SeparatorMenuItem())
946 is_played = episode['is_played']
947 if is_played:
948 item = gtk.ImageMenuItem(_('Mark as unplayed'))
949 item.set_image( gtk.image_new_from_stock( gtk.STOCK_CANCEL, gtk.ICON_SIZE_MENU))
950 item.connect( 'activate', lambda w: self.on_item_toggle_played_activate( w, False, False))
951 menu.append(self.set_finger_friendly(item))
952 else:
953 item = gtk.ImageMenuItem(_('Mark as played'))
954 item.set_image( gtk.image_new_from_stock( gtk.STOCK_APPLY, gtk.ICON_SIZE_MENU))
955 item.connect( 'activate', lambda w: self.on_item_toggle_played_activate( w, False, True))
956 menu.append(self.set_finger_friendly(item))
958 is_locked = episode['is_locked']
959 if is_locked:
960 item = gtk.ImageMenuItem(_('Allow deletion'))
961 item.set_image(gtk.image_new_from_stock(gtk.STOCK_DIALOG_AUTHENTICATION, gtk.ICON_SIZE_MENU))
962 item.connect('activate', self.on_item_toggle_lock_activate)
963 menu.append(self.set_finger_friendly(item))
964 else:
965 item = gtk.ImageMenuItem(_('Prohibit deletion'))
966 item.set_image(gtk.image_new_from_stock(gtk.STOCK_DIALOG_AUTHENTICATION, gtk.ICON_SIZE_MENU))
967 item.connect('activate', self.on_item_toggle_lock_activate)
968 menu.append(self.set_finger_friendly(item))
970 if len(paths) == 1:
971 menu.append(gtk.SeparatorMenuItem())
972 # Single item, add episode information menu item
973 episode_url = model.get_value( model.get_iter( paths[0]), 0)
974 item = gtk.ImageMenuItem(_('Episode details'))
975 item.set_image( gtk.image_new_from_stock( gtk.STOCK_INFO, gtk.ICON_SIZE_MENU))
976 item.connect( 'activate', lambda w: self.on_treeAvailable_row_activated( self.treeAvailable))
977 menu.append(self.set_finger_friendly(item))
978 episode = self.active_channel.find_episode(episode_url)
979 # If we have it, also add episode website link
980 if episode and episode.link and episode.link != episode.url:
981 item = gtk.ImageMenuItem(_('Visit website'))
982 item.set_image(gtk.image_new_from_icon_name(WEB_BROWSER_ICON, gtk.ICON_SIZE_MENU))
983 item.connect('activate', lambda w: util.open_website(episode.link))
984 menu.append(self.set_finger_friendly(item))
986 if gpodder.interface == gpodder.MAEMO:
987 # Because we open the popup on left-click for Maemo,
988 # we also include a non-action to close the menu
989 menu.append(gtk.SeparatorMenuItem())
990 item = gtk.ImageMenuItem(_('Close this menu'))
991 item.set_image(gtk.image_new_from_stock(gtk.STOCK_CLOSE, gtk.ICON_SIZE_MENU))
992 menu.append(self.set_finger_friendly(item))
994 menu.show_all()
995 # Disable tooltips while we are showing the menu, so
996 # the tooltip will not appear over the menu
997 self.episode_list_can_tooltip = False
998 menu.connect('deactivate', lambda menushell: self.episode_list_allow_tooltips())
999 menu.popup( None, None, None, event.button, event.time)
1001 return True
1003 def set_title(self, new_title):
1004 self.default_title = new_title
1005 self.gPodder.set_title(new_title)
1007 def download_progress_updated( self, count, percentage):
1008 title = [ self.default_title ]
1010 total_speed = gl.format_filesize(services.download_status_manager.total_speed())
1012 if count == 1:
1013 title.append( _('downloading one file'))
1014 elif count > 1:
1015 title.append( _('downloading %d files') % count)
1017 if len(title) == 2:
1018 title[1] = ''.join( [ title[1], ' (%d%%, %s/s)' % (percentage, total_speed) ])
1020 self.gPodder.set_title( ' - '.join( title))
1022 # Have all the downloads completed?
1023 # If so execute user command if defined, else do nothing
1024 if count == 0:
1025 if len(gl.config.cmd_all_downloads_complete) > 0:
1026 Thread(target=gl.ext_command_thread, args=(self.notification,gl.config.cmd_all_downloads_complete)).start()
1028 def playback_episode(self, episode):
1029 (success, application) = gl.playback_episode(episode)
1030 if not success:
1031 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), ))
1032 self.updateComboBox(only_selected_channel=True)
1034 def treeAvailable_search_equal( self, model, column, key, iter, data = None):
1035 if model is None:
1036 return True
1038 key = key.lower()
1040 # columns, as defined in libpodcasts' get model method
1041 # 1 = episode title, 7 = description
1042 columns = (1, 7)
1044 for column in columns:
1045 value = model.get_value( iter, column).lower()
1046 if value.find( key) != -1:
1047 return False
1049 return True
1051 def change_menu_item(self, menuitem, icon=None, label=None):
1052 if icon is not None:
1053 menuitem.get_image().set_from_icon_name(icon, gtk.ICON_SIZE_MENU)
1054 if label is not None:
1055 label_widget = menuitem.get_child()
1056 label_widget.set_text(label)
1058 def play_or_download(self):
1059 if self.wNotebook.get_current_page() > 0:
1060 return
1062 ( can_play, can_download, can_transfer, can_cancel, can_delete ) = (False,)*5
1063 ( is_played, is_locked ) = (False,)*2
1065 open_instead_of_play = False
1067 selection = self.treeAvailable.get_selection()
1068 if selection.count_selected_rows() > 0:
1069 (model, paths) = selection.get_selected_rows()
1071 for path in paths:
1072 url = model.get_value( model.get_iter( path), 0)
1073 local_filename = model.get_value( model.get_iter( path), 8)
1075 episode = podcastItem.load(url, self.active_channel)
1077 if episode.file_type() not in ('audio', 'video'):
1078 open_instead_of_play = True
1080 if episode.was_downloaded(and_exists=True):
1081 can_play = True
1082 can_delete = True
1083 is_played = episode.is_played
1084 is_locked = episode.is_locked
1085 else:
1086 if services.download_status_manager.is_download_in_progress(url):
1087 can_cancel = True
1088 else:
1089 can_download = True
1091 if episode.file_type() == 'torrent':
1092 can_download = can_download or gl.config.use_gnome_bittorrent
1094 can_download = can_download and not can_cancel
1095 can_play = can_play and not can_cancel and not can_download
1096 can_transfer = can_play and gl.config.device_type != 'none'
1098 if open_instead_of_play:
1099 self.toolPlay.set_stock_id(gtk.STOCK_OPEN)
1100 can_transfer = False
1101 else:
1102 self.toolPlay.set_stock_id(gtk.STOCK_MEDIA_PLAY)
1104 self.toolPlay.set_sensitive( can_play)
1105 self.toolDownload.set_sensitive( can_download)
1106 self.toolTransfer.set_sensitive( can_transfer)
1107 self.toolCancel.set_sensitive( can_cancel)
1109 if can_cancel:
1110 self.item_cancel_download.show_all()
1111 else:
1112 self.item_cancel_download.hide_all()
1113 if can_download:
1114 self.itemDownloadSelected.show_all()
1115 else:
1116 self.itemDownloadSelected.hide_all()
1117 if can_play:
1118 if open_instead_of_play:
1119 self.itemOpenSelected.show_all()
1120 else:
1121 self.itemPlaySelected.show_all()
1122 self.itemDeleteSelected.show_all()
1123 self.item_toggle_played.show_all()
1124 self.item_toggle_lock.show_all()
1125 self.separator9.show_all()
1126 if is_played:
1127 self.change_menu_item(self.item_toggle_played, gtk.STOCK_CANCEL, _('Mark as unplayed'))
1128 else:
1129 self.change_menu_item(self.item_toggle_played, gtk.STOCK_APPLY, _('Mark as played'))
1130 if is_locked:
1131 self.change_menu_item(self.item_toggle_lock, gtk.STOCK_DIALOG_AUTHENTICATION, _('Allow deletion'))
1132 else:
1133 self.change_menu_item(self.item_toggle_lock, gtk.STOCK_DIALOG_AUTHENTICATION, _('Prohibit deletion'))
1134 else:
1135 self.itemPlaySelected.hide_all()
1136 self.itemOpenSelected.hide_all()
1137 self.itemDeleteSelected.hide_all()
1138 self.item_toggle_played.hide_all()
1139 self.item_toggle_lock.hide_all()
1140 self.separator9.hide_all()
1141 if can_play or can_download or can_cancel:
1142 self.item_episode_details.show_all()
1143 self.separator16.show_all()
1144 self.no_episode_selected.hide_all()
1145 else:
1146 self.item_episode_details.hide_all()
1147 self.separator16.hide_all()
1148 self.no_episode_selected.show_all()
1150 return (can_play, can_download, can_transfer, can_cancel, can_delete, open_instead_of_play)
1152 def download_status_updated( self):
1153 count = services.download_status_manager.count()
1154 if count:
1155 self.labelDownloads.set_text( _('Downloads (%d)') % count)
1156 else:
1157 self.labelDownloads.set_text( _('Downloads'))
1159 self.updateComboBox()
1161 def on_cbMaxDownloads_toggled(self, widget, *args):
1162 self.spinMaxDownloads.set_sensitive(self.cbMaxDownloads.get_active())
1164 def on_cbLimitDownloads_toggled(self, widget, *args):
1165 self.spinLimitDownloads.set_sensitive(self.cbLimitDownloads.get_active())
1167 def updateComboBox(self, selected_url=None, only_selected_channel=False):
1168 (model, iter) = self.treeChannels.get_selection().get_selected()
1170 if only_selected_channel:
1171 if iter and self.active_channel is not None:
1172 update_channel_model_by_iter( self.treeChannels.get_model(),
1173 iter, self.active_channel, self.channel_colors,
1174 self.cover_cache, *(gl.config.podcast_list_icon_size,)*2 )
1175 else:
1176 if model and iter and selected_url is None:
1177 # Get the URL of the currently-selected podcast
1178 selected_url = model.get_value(iter, 0)
1180 rect = self.treeChannels.get_visible_rect()
1181 self.treeChannels.set_model( channels_to_model( self.channels,
1182 self.channel_colors, self.cover_cache,
1183 *(gl.config.podcast_list_icon_size,)*2 ))
1184 util.idle_add(self.treeChannels.scroll_to_point, rect.x, rect.y)
1186 try:
1187 selected_path = (0,)
1188 # Find the previously-selected URL in the new
1189 # model if we have an URL (else select first)
1190 if selected_url is not None:
1191 model = self.treeChannels.get_model()
1192 pos = model.get_iter_first()
1193 while pos is not None:
1194 url = model.get_value(pos, 0)
1195 if url == selected_url:
1196 selected_path = model.get_path(pos)
1197 break
1198 pos = model.iter_next(pos)
1200 self.treeChannels.get_selection().select_path(selected_path)
1201 except:
1202 log( 'Cannot set selection on treeChannels', sender = self)
1203 self.on_treeChannels_cursor_changed( self.treeChannels)
1205 def updateTreeView( self):
1206 if self.channels and self.active_channel is not None:
1207 self.treeAvailable.set_model(self.active_channel.tree_model)
1208 self.treeAvailable.columns_autosize()
1209 self.play_or_download()
1210 else:
1211 if self.treeAvailable.get_model():
1212 self.treeAvailable.get_model().clear()
1214 def drag_data_received(self, widget, context, x, y, sel, ttype, time):
1215 (path, column, rx, ry) = self.treeChannels.get_path_at_pos( x, y) or (None,)*4
1217 dnd_channel = None
1218 if path is not None:
1219 model = self.treeChannels.get_model()
1220 iter = model.get_iter(path)
1221 url = model.get_value(iter, 0)
1222 for channel in self.channels:
1223 if channel.url == url:
1224 dnd_channel = channel
1225 break
1227 result = sel.data
1228 rl = result.strip().lower()
1229 if (rl.endswith('.jpg') or rl.endswith('.png') or rl.endswith('.gif') or rl.endswith('.svg')) and dnd_channel is not None:
1230 services.cover_downloader.replace_cover(dnd_channel, result)
1231 else:
1232 self.add_new_channel(result)
1234 def add_new_channel(self, result=None, ask_download_new=True, quiet=False, block=False):
1235 result = util.normalize_feed_url( result)
1237 if not result:
1238 title = _('URL scheme not supported')
1239 message = _('gPodder currently only supports URLs starting with <b>http://</b>, <b>feed://</b> or <b>ftp://</b>.')
1240 self.show_message( message, title)
1241 return
1243 for old_channel in self.channels:
1244 if old_channel.url == result:
1245 log( 'Channel already exists: %s', result)
1246 # Select the existing channel in combo box
1247 for i in range( len( self.channels)):
1248 if self.channels[i] == old_channel:
1249 self.treeChannels.get_selection().select_path( (i,))
1250 self.on_treeChannels_cursor_changed(self.treeChannels)
1251 break
1252 self.show_message( _('You have already subscribed to this podcast: %s') % (
1253 saxutils.escape( old_channel.title), ), _('Already added'))
1254 return
1256 self.entryAddChannel.set_text(_('Downloading feed...'))
1257 self.entryAddChannel.set_sensitive(False)
1258 self.btnAddChannel.set_sensitive(False)
1259 args = (result, self.add_new_channel_finish, ask_download_new, quiet)
1260 thread = Thread( target=self.add_new_channel_proc, args=args )
1261 thread.start()
1263 while block and thread.isAlive():
1264 time.sleep(0.05)
1266 def add_new_channel_proc( self, url, callback, *callback_args):
1267 log( 'Adding new channel: %s', url)
1268 try:
1269 channel = podcastChannel.load(url=url, create=True)
1270 except Exception, e:
1271 log('Error in podcastChannel.load(%s): %s', url, e, traceback=True, sender=self)
1272 channel = None
1274 util.idle_add( callback, channel, url, *callback_args )
1276 def add_new_channel_finish( self, channel, url, ask_download_new, quiet ):
1277 if channel is not None:
1278 self.channels.append( channel)
1279 save_channels( self.channels)
1280 if not quiet:
1281 # download changed channels and select the new episode in the UI afterwards
1282 self.update_feed_cache(force_update=False, select_url_afterwards=channel.url)
1284 (username, password) = util.username_password_from_url( url)
1285 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')):
1286 channel.username = username
1287 channel.password = password
1288 log('Saving authentication data for episode downloads..', sender = self)
1289 channel.save()
1291 if ask_download_new:
1292 new_episodes = channel.get_new_episodes()
1293 if len(new_episodes):
1294 self.new_episodes_show(new_episodes)
1295 else:
1296 # Ok, the URL is not a channel, or there is some other
1297 # error - let's see if it's a web page or OPML file...
1298 try:
1299 data = urllib2.urlopen(url).read().lower()
1300 if '</opml>' in data:
1301 # This looks like an OPML feed
1302 self.on_item_import_from_file_activate(None, url)
1303 return
1304 elif '</html>' in data:
1305 # This looks like a web page
1306 title = _('The URL is a website')
1307 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.)')
1308 if self.show_confirmation(message, title):
1309 util.open_website(url)
1310 return
1311 except Exception, e:
1312 log('Error trying to handle the URL as OPML or web page: %s', e, sender=self)
1314 title = _('Error adding podcast')
1315 message = _('The podcast could not be added. Please check the spelling of the URL or try again later.')
1316 self.show_message( message, title)
1318 self.entryAddChannel.set_text('')
1319 self.entryAddChannel.set_sensitive(True)
1320 self.btnAddChannel.set_sensitive(True)
1321 self.update_podcasts_tab()
1324 def update_feed_cache_finish_callback(self, channels=None,
1325 notify_no_new_episodes=False, select_url_afterwards=None):
1327 db.commit()
1329 self.updating_feed_cache = False
1330 self.hboxUpdateFeeds.hide_all()
1331 self.btnUpdateFeeds.show_all()
1332 self.itemUpdate.set_sensitive(True)
1333 self.itemUpdateChannel.set_sensitive(True)
1335 # If we want to select a specific podcast (via its URL)
1336 # after the update, we give it to updateComboBox here to
1337 # select exactly this podcast after updating the view
1338 self.updateComboBox(selected_url=select_url_afterwards)
1340 if self.tray_icon:
1341 self.tray_icon.set_status(None)
1342 if self.minimized:
1343 new_episodes = []
1344 # look for new episodes to notify
1345 for channel in self.channels:
1346 for episode in channel.get_new_episodes():
1347 if not episode in self.already_notified_new_episodes:
1348 new_episodes.append(episode)
1349 self.already_notified_new_episodes.append(episode)
1350 # notify new episodes
1352 if len(new_episodes) == 0:
1353 if notify_no_new_episodes and self.tray_icon is not None:
1354 msg = _('No new episodes available for download')
1355 self.tray_icon.send_notification(msg)
1356 return
1357 elif len(new_episodes) == 1:
1358 title = _('gPodder has found %s') % (_('one new episode:'),)
1359 else:
1360 title = _('gPodder has found %s') % (_('%i new episodes:') % len(new_episodes))
1361 message = self.tray_icon.format_episode_list(new_episodes)
1363 #auto download new episodes
1364 if gl.config.auto_download_when_minimized:
1365 message += '\n<i>(%s...)</i>' % _('downloading')
1366 self.download_episode_list(new_episodes)
1367 self.tray_icon.send_notification(message, title)
1368 return
1370 # open the episodes selection dialog
1371 self.channels = load_channels()
1372 self.updateComboBox()
1373 if not self.feed_cache_update_cancelled:
1374 self.download_all_new(channels=channels)
1376 def update_feed_cache_callback(self, progressbar, title, position, count):
1377 progression = _('Updated %s (%d/%d)')%(title, position+1, count)
1378 progressbar.set_text(progression)
1379 if self.tray_icon:
1380 self.tray_icon.set_status(
1381 self.tray_icon.STATUS_UPDATING_FEED_CACHE, progression )
1382 if count > 0:
1383 progressbar.set_fraction(float(position)/float(count))
1385 def update_feed_cache_proc( self, channel, total_channels, semaphore,
1386 callback_proc, finish_proc):
1388 semaphore.acquire()
1389 if not self.feed_cache_update_cancelled:
1390 try:
1391 channel.update()
1392 except:
1393 log('Darn SQLite LOCK!', sender=self, traceback=True)
1395 # By the time we get here the update may have already been cancelled
1396 if not self.feed_cache_update_cancelled:
1397 callback_proc(channel.title, self.updated_feeds, total_channels)
1399 self.updated_feeds += 1
1400 self.treeview_channel_set_color( channel, 'default' )
1401 channel.update_flag = False
1403 semaphore.release()
1404 if self.updated_feeds == total_channels:
1405 finish_proc()
1407 def on_btnCancelFeedUpdate_clicked(self, widget):
1408 self.pbFeedUpdate.set_text(_('Cancelling...'))
1409 self.feed_cache_update_cancelled = True
1411 def update_feed_cache(self, channels=None, force_update=True,
1412 notify_no_new_episodes=False, select_url_afterwards=None):
1414 if self.updating_feed_cache:
1415 return
1417 if not force_update:
1418 self.channels = load_channels()
1419 self.updateComboBox()
1420 return
1422 self.updating_feed_cache = True
1423 self.itemUpdate.set_sensitive(False)
1424 self.itemUpdateChannel.set_sensitive(False)
1426 if self.tray_icon:
1427 self.tray_icon.set_status(self.tray_icon.STATUS_UPDATING_FEED_CACHE)
1429 if channels is None:
1430 channels = self.channels
1432 if len(channels) == 1:
1433 text = _('Updating %d feed.')
1434 else:
1435 text = _('Updating %d feeds.')
1436 self.pbFeedUpdate.set_text( text % len(channels))
1437 self.pbFeedUpdate.set_fraction(0)
1439 # let's get down to business..
1440 callback_proc = lambda title, pos, count: util.idle_add(
1441 self.update_feed_cache_callback, self.pbFeedUpdate, title, pos, count )
1442 finish_proc = lambda: util.idle_add( self.update_feed_cache_finish_callback,
1443 channels, notify_no_new_episodes, select_url_afterwards )
1445 self.updated_feeds = 0
1446 self.feed_cache_update_cancelled = False
1447 self.btnUpdateFeeds.hide_all()
1448 self.hboxUpdateFeeds.show_all()
1449 semaphore = Semaphore(gl.config.max_simulaneous_feeds_updating)
1451 for channel in channels:
1452 self.treeview_channel_set_color( channel, 'updating' )
1453 channel.update_flag = True
1454 args = (channel, len(channels), semaphore, callback_proc, finish_proc)
1455 thread = Thread( target = self.update_feed_cache_proc, args = args)
1456 thread.start()
1458 def treeview_channel_set_color( self, channel, color ):
1459 if self.treeChannels.get_model():
1460 if color in self.channel_colors:
1461 self.treeChannels.get_model().set(channel.iter, 8, self.channel_colors[color])
1462 else:
1463 self.treeChannels.get_model().set(channel.iter, 8, color)
1465 def on_gPodder_delete_event(self, widget, *args):
1466 """Called when the GUI wants to close the window
1467 Displays a confirmation dialog (and closes/hides gPodder)
1470 downloading = services.download_status_manager.has_items()
1472 # Only iconify if we are using the window's "X" button,
1473 # but not when we are using "Quit" in the menu or toolbar
1474 if not gl.config.on_quit_ask and gl.config.on_quit_systray and self.tray_icon and widget.name not in ('toolQuit', 'itemQuit'):
1475 self.iconify_main_window()
1476 elif gl.config.on_quit_ask or downloading:
1477 if gpodder.interface == gpodder.MAEMO:
1478 result = self.show_confirmation(_('Do you really want to quit gPodder now?'))
1479 if result:
1480 self.close_gpodder()
1481 else:
1482 return True
1483 dialog = gtk.MessageDialog(self.gPodder, gtk.DIALOG_MODAL, gtk.MESSAGE_QUESTION, gtk.BUTTONS_NONE)
1484 dialog.add_button(gtk.STOCK_CANCEL, gtk.RESPONSE_CANCEL)
1485 dialog.add_button(gtk.STOCK_QUIT, gtk.RESPONSE_CLOSE)
1487 title = _('Quit gPodder')
1488 if downloading:
1489 message = _('You are downloading episodes. If you close gPodder now, the downloads will be aborted.')
1490 else:
1491 message = _('Do you really want to quit gPodder now?')
1493 dialog.set_title(title)
1494 dialog.set_markup('<span weight="bold" size="larger">%s</span>\n\n%s'%(title, message))
1495 if not downloading:
1496 cb_ask = gtk.CheckButton(_("Don't ask me again"))
1497 dialog.vbox.pack_start(cb_ask)
1498 cb_ask.show_all()
1500 result = dialog.run()
1501 dialog.destroy()
1503 if result == gtk.RESPONSE_CLOSE:
1504 if not downloading and cb_ask.get_active() == True:
1505 gl.config.on_quit_ask = False
1506 self.close_gpodder()
1507 else:
1508 self.close_gpodder()
1510 return True
1512 def close_gpodder(self):
1513 """ clean everything and exit properly
1515 if self.channels:
1516 if not save_channels(self.channels):
1517 self.show_message(_('Please check your permissions and free disk space.'), _('Error saving podcast list'))
1519 services.download_status_manager.cancel_all()
1520 db.commit()
1522 self.gtk_main_quit()
1523 sys.exit( 0)
1525 def get_old_episodes(self):
1526 episodes = []
1527 for channel in self.channels:
1528 for episode in channel.get_downloaded_episodes():
1529 if episode.is_old() and not episode.is_locked and episode.is_played:
1530 episodes.append(episode)
1531 return episodes
1533 def for_each_selected_episode_url( self, callback):
1534 ( model, paths ) = self.treeAvailable.get_selection().get_selected_rows()
1535 for path in paths:
1536 url = model.get_value( model.get_iter( path), 0)
1537 try:
1538 callback( url)
1539 except Exception, e:
1540 log( 'Warning: Error in for_each_selected_episode_url for URL %s: %s', url, e, sender = self)
1542 self.updateComboBox(only_selected_channel=True)
1544 def delete_episode_list( self, episodes, confirm = True):
1545 if len(episodes) == 0:
1546 return
1548 if len(episodes) == 1:
1549 message = _('Do you really want to delete this episode?')
1550 else:
1551 message = _('Do you really want to delete %d episodes?') % len(episodes)
1553 if confirm and self.show_confirmation( message, _('Delete episodes')) == False:
1554 return
1556 for episode in episodes:
1557 log('Deleting episode: %s', episode.title, sender = self)
1558 episode.delete_from_disk()
1560 self.download_status_updated()
1562 def on_itemRemoveOldEpisodes_activate( self, widget):
1563 columns = (
1564 ('title_and_description', None, None, _('Episode')),
1565 ('channel_prop', None, None, _('Podcast')),
1566 ('filesize_prop', 'length', gobject.TYPE_INT, _('Size')),
1567 ('pubdate_prop', 'pubDate', gobject.TYPE_INT, _('Released')),
1568 ('played_prop', None, None, _('Status')),
1569 ('age_prop', None, None, _('Downloaded')),
1572 selection_buttons = {
1573 _('Select played'): lambda episode: episode.is_played,
1574 _('Select older than %d days') % gl.config.episode_old_age: lambda episode: episode.is_old(),
1577 instructions = _('Select the episodes you want to delete from your hard disk.')
1579 episodes = []
1580 selected = []
1581 for channel in self.channels:
1582 for episode in channel.get_downloaded_episodes():
1583 if not episode.is_locked:
1584 episodes.append(episode)
1585 selected.append(episode.is_played)
1587 gPodderEpisodeSelector( title = _('Remove old episodes'), instructions = instructions, \
1588 episodes = episodes, selected = selected, columns = columns, \
1589 stock_ok_button = gtk.STOCK_DELETE, callback = self.delete_episode_list, \
1590 selection_buttons = selection_buttons)
1592 def mark_selected_episodes_new(self):
1593 callback = lambda url: self.active_channel.find_episode(url).mark_new()
1594 self.for_each_selected_episode_url(callback)
1596 def mark_selected_episodes_old(self):
1597 callback = lambda url: self.active_channel.find_episode(url).mark_old()
1598 self.for_each_selected_episode_url(callback)
1600 def on_item_toggle_played_activate( self, widget, toggle = True, new_value = False):
1601 if toggle:
1602 callback = lambda url: db.mark_episode(url, is_played=True, toggle=True)
1603 else:
1604 callback = lambda url: db.mark_episode(url, is_played=new_value)
1606 self.for_each_selected_episode_url(callback)
1608 def on_item_toggle_lock_activate(self, widget, toggle=True, new_value=False):
1609 if toggle:
1610 callback = lambda url: db.mark_episode(url, is_locked=True, toggle=True)
1611 else:
1612 callback = lambda url: db.mark_episode(url, is_locked=new_value)
1614 self.for_each_selected_episode_url(callback)
1616 def on_item_email_subscriptions_activate(self, widget):
1617 if not self.channels:
1618 self.show_message(_('Your subscription list is empty.'), _('Could not send list'))
1619 elif not gl.send_subscriptions():
1620 self.show_message(_('There was an error sending your subscription list via e-mail.'), _('Could not send list'))
1622 def on_item_show_url_entry_activate(self, widget):
1623 gl.config.show_podcast_url_entry = self.item_show_url_entry.get_active()
1625 def on_itemUpdateChannel_activate(self, widget=None):
1626 self.update_feed_cache(channels=[self.active_channel,])
1628 def on_itemUpdate_activate(self, widget, notify_no_new_episodes=False):
1629 restore_from = can_restore_from_opml()
1631 if self.channels:
1632 self.update_feed_cache(notify_no_new_episodes=notify_no_new_episodes)
1633 elif restore_from is not None:
1634 title = _('Database upgrade required')
1635 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?')
1636 if self.show_confirmation(message, title):
1637 add_callback = lambda url: self.add_new_channel(url, False, True)
1638 w = gtk.Dialog(_('Migrating to SQLite'), self.gPodder, 0, (gtk.STOCK_CLOSE, gtk.RESPONSE_ACCEPT))
1639 w.set_has_separator(False)
1640 w.set_response_sensitive(gtk.RESPONSE_ACCEPT, False)
1641 w.set_default_size(500, -1)
1642 pb = gtk.ProgressBar()
1643 l = gtk.Label()
1644 l.set_padding(6, 3)
1645 l.set_markup('<b><big>%s</big></b>' % _('SQLite migration'))
1646 l.set_alignment(0.0, 0.5)
1647 w.vbox.pack_start(l)
1648 l = gtk.Label()
1649 l.set_padding(6, 3)
1650 l.set_alignment(0.0, 0.5)
1651 l.set_text(_('Please wait while your settings are converted.'))
1652 w.vbox.pack_start(l)
1653 w.vbox.pack_start(pb)
1654 lb = gtk.Label()
1655 lb.set_ellipsize(pango.ELLIPSIZE_END)
1656 lb.set_alignment(0.0, 0.5)
1657 lb.set_padding(6, 6)
1658 w.vbox.pack_start(lb)
1660 def set_pb_status(pb, lb, fraction, text):
1661 pb.set_fraction(float(fraction)/100.0)
1662 pb.set_text('%.0f %%' % fraction)
1663 lb.set_markup('<i>%s</i>' % saxutils.escape(text))
1664 while gtk.events_pending():
1665 gtk.main_iteration(False)
1666 status_callback = lambda fraction, text: set_pb_status(pb, lb, fraction, text)
1667 get_localdb = lambda channel: LocalDBReader(channel.url).read(channel.index_file)
1668 w.show_all()
1669 start = datetime.datetime.now()
1670 gl.migrate_to_sqlite(add_callback, status_callback, load_channels, get_localdb)
1671 # Refresh the view with the updated episodes
1672 self.updateComboBox()
1673 time_taken = str(datetime.datetime.now()-start)
1674 status_callback(100.0, _('Migration finished in %s') % time_taken)
1675 w.set_response_sensitive(gtk.RESPONSE_ACCEPT, True)
1676 w.run()
1677 w.destroy()
1678 else:
1679 title = _('Import podcasts from the web')
1680 message = _('Your podcast list is empty. Do you want to see a list of example podcasts you can subscribe to?')
1681 if self.show_confirmation(message, title):
1682 self.on_itemImportChannels_activate(self, widget)
1684 def download_episode_list( self, episodes):
1685 services.download_status_manager.start_batch_mode()
1686 for episode in episodes:
1687 log('Downloading episode: %s', episode.title, sender = self)
1688 filename = episode.local_filename()
1689 if not os.path.exists( filename) and not services.download_status_manager.is_download_in_progress( episode.url):
1690 download.DownloadThread( episode.channel, episode, self.notification).start()
1691 services.download_status_manager.end_batch_mode()
1693 def new_episodes_show(self, episodes):
1694 columns = (
1695 ('title_and_description', None, None, _('Episode')),
1696 ('channel_prop', None, None, _('Podcast')),
1697 ('filesize_prop', 'length', gobject.TYPE_INT, _('Size')),
1698 ('pubdate_prop', 'pubDate', gobject.TYPE_INT, _('Released')),
1701 if len(episodes) > 0:
1702 instructions = _('Select the episodes you want to download now.')
1704 gPodderEpisodeSelector(title=_('New episodes available'), instructions=instructions, \
1705 episodes=episodes, columns=columns, selected_default=True, \
1706 stock_ok_button = 'gpodder-download', \
1707 callback=self.download_episode_list)
1708 else:
1709 title = _('No new episodes')
1710 message = _('No new episodes to download.\nPlease check for new episodes later.')
1711 self.show_message(message, title)
1713 def on_itemDownloadAllNew_activate(self, widget, *args):
1714 self.download_all_new()
1716 def download_all_new(self, channels=None):
1717 if channels is None:
1718 channels = self.channels
1719 episodes = []
1720 for channel in channels:
1721 for episode in channel.get_new_episodes():
1722 episodes.append(episode)
1723 self.new_episodes_show(episodes)
1725 def get_all_episodes(self, exclude_nonsignificant=True ):
1726 """'exclude_nonsignificant' will exclude non-downloaded episodes
1727 and all episodes from channels that are set to skip when syncing"""
1728 episode_list = []
1729 for channel in self.channels:
1730 if not channel.sync_to_devices and exclude_nonsignificant:
1731 log('Skipping channel: %s', channel.title, sender=self)
1732 continue
1733 for episode in channel.get_all_episodes():
1734 if episode.was_downloaded(and_exists=True) or not exclude_nonsignificant:
1735 episode_list.append(episode)
1736 return episode_list
1738 def ipod_delete_played(self, device):
1739 all_episodes = self.get_all_episodes( exclude_nonsignificant=False )
1740 episodes_on_device = device.get_all_tracks()
1741 for local_episode in all_episodes:
1742 device_episode = device.episode_on_device(local_episode)
1743 if device_episode and ( local_episode.is_played and not local_episode.is_locked
1744 or local_episode.state == db.STATE_DELETED ):
1745 log("mp3_player_delete_played: removing %s" % device_episode.title)
1746 device.remove_track(device_episode)
1748 def on_sync_to_ipod_activate(self, widget, episodes=None):
1749 # make sure gpod is available before even trying to sync
1750 if gl.config.device_type == 'ipod' and not sync.gpod_available:
1751 title = _('Cannot Sync To iPod')
1752 message = _('Please install the libgpod python bindings (python-gpod) and restart gPodder to continue.')
1753 self.notification( message, title )
1754 return
1755 elif gl.config.device_type == 'mtp' and not sync.pymtp_available:
1756 title = _('Cannot sync to MTP device')
1757 message = _('Please install the libmtp python bindings (python-pymtp) and restart gPodder to continue.')
1758 self.notification( message, title )
1759 return
1761 device = sync.open_device()
1762 device.register( 'post-done', self.sync_to_ipod_completed )
1764 if device is None:
1765 title = _('No device configured')
1766 message = _('To use the synchronization feature, please configure your device in the preferences dialog first.')
1767 self.notification(message, title)
1768 return
1770 if not device.open():
1771 title = _('Cannot open device')
1772 message = _('There has been an error opening your device.')
1773 self.notification(message, title)
1774 return
1776 sync_all_episodes = not bool(episodes)
1778 if episodes is None:
1779 episodes = self.get_all_episodes()
1781 # make sure we have enough space on the device
1782 total_size = 0
1783 free_space = device.get_free_space()
1784 for episode in episodes:
1785 if not device.episode_on_device(episode):
1786 total_size += util.calculate_size(str(episode.local_filename()))
1788 if total_size > free_space:
1789 # can be negative because of the 10 MiB for reserved for the iTunesDB
1790 free_space = max( free_space, 0 )
1791 log('(gpodder.sync) Not enough free space. Transfer size = %d, Free space = %d', total_size, free_space)
1792 title = _('Not enough space left on device.')
1793 message = _('%s remaining on device.\nPlease free up %s and try again.' % (
1794 util.format_filesize( free_space ), util.format_filesize( total_size - free_space )))
1795 self.notification(message, title)
1796 else:
1797 # start syncing!
1798 gPodderSync(device=device, gPodder=self)
1799 Thread(target=self.sync_to_ipod_thread, args=(widget, device, sync_all_episodes, episodes)).start()
1800 if self.tray_icon:
1801 self.tray_icon.set_synchronisation_device(device)
1803 def sync_to_ipod_completed(self, device, successful_sync):
1804 device.unregister( 'post-done', self.sync_to_ipod_completed )
1806 if self.tray_icon:
1807 self.tray_icon.release_synchronisation_device()
1809 if not successful_sync:
1810 title = _('Error closing device')
1811 message = _('There has been an error closing your device.')
1812 self.notification(message, title)
1814 # update model for played state updates after sync
1815 util.idle_add(self.updateComboBox)
1817 def sync_to_ipod_thread(self, widget, device, sync_all_episodes, episodes=None):
1818 if sync_all_episodes:
1819 device.add_tracks(episodes)
1820 # 'only_sync_not_played' must be used or else all the played
1821 # tracks will be copied then immediately deleted
1822 if gl.config.mp3_player_delete_played and gl.config.only_sync_not_played:
1823 self.ipod_delete_played(device)
1824 else:
1825 device.add_tracks(episodes, force_played=True)
1826 device.close()
1828 def ipod_cleanup_callback(self, device, tracks):
1829 title = _('Delete podcasts from device?')
1830 message = _('The selected episodes will be removed from your device. This cannot be undone. Files in your gPodder library will be unaffected. Do you really want to delete these episodes from your device?')
1831 if len(tracks) > 0 and self.show_confirmation(message, title):
1832 device.remove_tracks(tracks)
1834 if not device.close():
1835 title = _('Error closing device')
1836 message = _('There has been an error closing your device.')
1837 self.show_message(message, title)
1838 return
1840 def on_cleanup_ipod_activate(self, widget, *args):
1841 columns = (
1842 ('title', None, None, _('Episode')),
1843 ('podcast', None, None, _('Podcast')),
1844 ('filesize', None, None, _('Size')),
1845 ('modified', None, None, _('Copied')),
1846 ('playcount', None, None, _('Play count')),
1847 ('released', None, None, _('Released')),
1850 device = sync.open_device()
1852 if device is None:
1853 title = _('No device configured')
1854 message = _('To use the synchronization feature, please configure your device in the preferences dialog first.')
1855 self.show_message(message, title)
1856 return
1858 if not device.open():
1859 title = _('Cannot open device')
1860 message = _('There has been an error opening your device.')
1861 self.show_message(message, title)
1862 return
1864 gPodderSync(device=device, gPodder=self)
1866 tracks = device.get_all_tracks()
1867 if len(tracks) > 0:
1868 remove_tracks_callback = lambda tracks: self.ipod_cleanup_callback(device, tracks)
1869 wanted_columns = []
1870 for key, sort_name, sort_type, caption in columns:
1871 want_this_column = False
1872 for track in tracks:
1873 if getattr(track, key) is not None:
1874 want_this_column = True
1875 break
1877 if want_this_column:
1878 wanted_columns.append((key, sort_name, sort_type, caption))
1879 title = _('Remove podcasts from device')
1880 instructions = _('Select the podcast episodes you want to remove from your device.')
1881 gPodderEpisodeSelector(title=title, instructions=instructions, episodes=tracks, columns=wanted_columns, \
1882 stock_ok_button=gtk.STOCK_DELETE, callback=remove_tracks_callback, tooltip_attribute=None)
1883 else:
1884 title = _('No files on device')
1885 message = _('The devices contains no files to be removed.')
1886 self.show_message(message, title)
1887 device.close()
1889 def show_hide_tray_icon(self):
1890 if gl.config.display_tray_icon and have_trayicon and self.tray_icon is None:
1891 self.tray_icon = trayicon.GPodderStatusIcon(self, scalable_dir)
1892 elif not gl.config.display_tray_icon and self.tray_icon is not None:
1893 self.tray_icon.set_visible(False)
1894 del self.tray_icon
1895 self.tray_icon = None
1897 if gl.config.minimize_to_tray and self.tray_icon:
1898 self.tray_icon.set_visible(self.minimized)
1899 elif self.tray_icon:
1900 self.tray_icon.set_visible(True)
1902 def on_itemShowToolbar_activate(self, widget):
1903 gl.config.show_toolbar = self.itemShowToolbar.get_active()
1905 def on_itemShowDescription_activate(self, widget):
1906 gl.config.episode_list_descriptions = self.itemShowDescription.get_active()
1908 def update_item_device( self):
1909 if gl.config.device_type != 'none':
1910 self.itemDevice.show_all()
1911 (label,) = self.itemDevice.get_children()
1912 label.set_text(gl.get_device_name())
1913 else:
1914 self.itemDevice.hide_all()
1916 def properties_closed( self):
1917 self.show_hide_tray_icon()
1918 self.update_item_device()
1919 self.updateComboBox()
1921 def on_itemPreferences_activate(self, widget, *args):
1922 if gpodder.interface == gpodder.GUI:
1923 gPodderProperties(callback_finished=self.properties_closed, user_apps_reader=self.user_apps_reader)
1924 else:
1925 gPodderMaemoPreferences()
1927 def on_add_new_google_search(self, widget, *args):
1928 def add_google_video_search(query):
1929 self.add_new_channel('http://video.google.com/videofeed?type=search&q='+urllib.quote(query)+'&so=1&num=250&output=rss')
1931 gPodderAddPodcastDialog(url_callback=add_google_video_search, custom_title=_('Add Google Video search'), custom_label=_('Search for:'))
1933 def on_itemAddChannel_activate(self, widget, *args):
1934 if gpodder.interface == gpodder.MAEMO or not gl.config.show_podcast_url_entry:
1935 gPodderAddPodcastDialog(url_callback=self.add_new_channel)
1936 else:
1937 if self.channelPaned.get_position() < 200:
1938 self.channelPaned.set_position( 200)
1939 self.entryAddChannel.grab_focus()
1941 def on_itemEditChannel_activate(self, widget, *args):
1942 if self.active_channel is None:
1943 title = _('No podcast selected')
1944 message = _('Please select a podcast in the podcasts list to edit.')
1945 self.show_message( message, title)
1946 return
1948 gPodderChannel(channel=self.active_channel, callback_closed=lambda: self.updateComboBox(only_selected_channel=True), callback_change_url=self.change_channel_url)
1950 def change_channel_url(self, old_url, new_url):
1951 channel = None
1952 try:
1953 channel = podcastChannel.load(url=new_url, create=True)
1954 except:
1955 channel = None
1957 if channel is None:
1958 self.show_message(_('The specified URL is invalid. The old URL has been used instead.'), _('Invalid URL'))
1959 return
1961 for channel in self.channels:
1962 if channel.url == old_url:
1963 log('=> change channel url from %s to %s', old_url, new_url)
1964 old_save_dir = channel.save_dir
1965 channel.url = new_url
1966 new_save_dir = channel.save_dir
1967 log('old save dir=%s', old_save_dir, sender=self)
1968 log('new save dir=%s', new_save_dir, sender=self)
1969 files = glob.glob(os.path.join(old_save_dir, '*'))
1970 log('moving %d files to %s', len(files), new_save_dir, sender=self)
1971 for file in files:
1972 log('moving %s', file, sender=self)
1973 shutil.move(file, new_save_dir)
1974 try:
1975 os.rmdir(old_save_dir)
1976 except:
1977 log('Warning: cannot delete %s', old_save_dir, sender=self)
1979 save_channels(self.channels)
1980 # update feed cache and select the podcast with the new URL afterwards
1981 self.update_feed_cache(force_update=False, select_url_afterwards=new_url)
1983 def on_itemRemoveChannel_activate(self, widget, *args):
1984 try:
1985 if gpodder.interface == gpodder.GUI:
1986 dialog = gtk.MessageDialog(self.gPodder, gtk.DIALOG_MODAL, gtk.MESSAGE_QUESTION, gtk.BUTTONS_NONE)
1987 dialog.add_button(gtk.STOCK_NO, gtk.RESPONSE_NO)
1988 dialog.add_button(gtk.STOCK_YES, gtk.RESPONSE_YES)
1990 title = _('Remove podcast and episodes?')
1991 message = _('Do you really want to remove <b>%s</b> and all downloaded episodes?') % saxutils.escape(self.active_channel.title)
1993 dialog.set_title(title)
1994 dialog.set_markup('<span weight="bold" size="larger">%s</span>\n\n%s'%(title, message))
1996 cb_ask = gtk.CheckButton(_('Do not delete my downloaded episodes'))
1997 dialog.vbox.pack_start(cb_ask)
1998 cb_ask.show_all()
1999 affirmative = gtk.RESPONSE_YES
2000 elif gpodder.interface == gpodder.MAEMO:
2001 cb_ask = gtk.CheckButton('') # dummy check button
2002 dialog = hildon.Note('confirmation', (self.gPodder, _('Do you really want to remove this podcast and all downloaded episodes?')))
2003 affirmative = gtk.RESPONSE_OK
2005 result = dialog.run()
2006 dialog.destroy()
2008 if result == affirmative:
2009 # delete downloaded episodes only if checkbox is unchecked
2010 if cb_ask.get_active() == False:
2011 self.active_channel.remove_downloaded()
2012 else:
2013 log('Not removing downloaded episodes', sender=self)
2015 # only delete partial files if we do not have any downloads in progress
2016 delete_partial = not services.download_status_manager.has_items()
2017 gl.clean_up_downloads(delete_partial)
2019 # cancel any active downloads from this channel
2020 if not delete_partial:
2021 for episode in self.active_channel.get_all_episodes():
2022 services.download_status_manager.cancel_by_url(episode.url)
2024 # get the URL of the podcast we want to select next
2025 position = self.channels.index(self.active_channel)
2026 if position == len(self.channels)-1:
2027 # this is the last podcast, so select the URL
2028 # of the item before this one (i.e. the "new last")
2029 select_url = self.channels[position-1].url
2030 else:
2031 # there is a podcast after the deleted one, so
2032 # we simply select the one that comes after it
2033 select_url = self.channels[position+1].url
2035 # Remove the channel
2036 self.active_channel.delete()
2037 self.channels.remove(self.active_channel)
2038 save_channels(self.channels)
2040 # Re-load the channels and select the desired new channel
2041 self.update_feed_cache(force_update=False, select_url_afterwards=select_url)
2042 except:
2043 log('There has been an error removing the channel.', traceback=True, sender=self)
2044 self.update_podcasts_tab()
2046 def get_opml_filter(self):
2047 filter = gtk.FileFilter()
2048 filter.add_pattern('*.opml')
2049 filter.add_pattern('*.xml')
2050 filter.set_name(_('OPML files')+' (*.opml, *.xml)')
2051 return filter
2053 def on_item_import_from_file_activate(self, widget, filename=None):
2054 if filename is None:
2055 if gpodder.interface == gpodder.GUI:
2056 dlg = gtk.FileChooserDialog(title=_('Import from OPML'), parent=None, action=gtk.FILE_CHOOSER_ACTION_OPEN)
2057 dlg.add_button(gtk.STOCK_CANCEL, gtk.RESPONSE_CANCEL)
2058 dlg.add_button(gtk.STOCK_OPEN, gtk.RESPONSE_OK)
2059 elif gpodder.interface == gpodder.MAEMO:
2060 dlg = hildon.FileChooserDialog(self.gPodder, gtk.FILE_CHOOSER_ACTION_OPEN)
2061 dlg.set_filter(self.get_opml_filter())
2062 response = dlg.run()
2063 filename = None
2064 if response == gtk.RESPONSE_OK:
2065 filename = dlg.get_filename()
2066 dlg.destroy()
2068 if filename is not None:
2069 gPodderOpmlLister(custom_title=_('Import podcasts from OPML file'), hide_url_entry=True).get_channels_from_url(filename, lambda url: self.add_new_channel(url,False,block=True), lambda: self.on_itemDownloadAllNew_activate(self.gPodder))
2071 def on_itemExportChannels_activate(self, widget, *args):
2072 if not self.channels:
2073 title = _('Nothing to export')
2074 message = _('Your list of podcast subscriptions is empty. Please subscribe to some podcasts first before trying to export your subscription list.')
2075 self.show_message( message, title)
2076 return
2078 if gpodder.interface == gpodder.GUI:
2079 dlg = gtk.FileChooserDialog(title=_('Export to OPML'), parent=self.gPodder, action=gtk.FILE_CHOOSER_ACTION_SAVE)
2080 dlg.add_button(gtk.STOCK_CANCEL, gtk.RESPONSE_CANCEL)
2081 dlg.add_button(gtk.STOCK_SAVE, gtk.RESPONSE_OK)
2082 elif gpodder.interface == gpodder.MAEMO:
2083 dlg = hildon.FileChooserDialog(self.gPodder, gtk.FILE_CHOOSER_ACTION_SAVE)
2084 dlg.set_filter(self.get_opml_filter())
2085 response = dlg.run()
2086 if response == gtk.RESPONSE_OK:
2087 filename = dlg.get_filename()
2088 dlg.destroy()
2089 exporter = opml.Exporter( filename)
2090 if exporter.write(self.channels):
2091 if len(self.channels) == 1:
2092 title = _('One subscription exported')
2093 else:
2094 title = _('%d subscriptions exported') % len(self.channels)
2095 self.show_message(_('Your podcast list has been successfully exported.'), title)
2096 else:
2097 self.show_message( _('Could not export OPML to file. Please check your permissions.'), _('OPML export failed'))
2098 else:
2099 dlg.destroy()
2101 def on_itemImportChannels_activate(self, widget, *args):
2102 gPodderOpmlLister().get_channels_from_url(gl.config.opml_url, lambda url: self.add_new_channel(url,False,block=True), lambda: self.on_itemDownloadAllNew_activate(self.gPodder))
2104 def on_homepage_activate(self, widget, *args):
2105 util.open_website(app_website)
2107 def on_wiki_activate(self, widget, *args):
2108 util.open_website('http://wiki.gpodder.org/')
2110 def on_bug_tracker_activate(self, widget, *args):
2111 util.open_website('http://bugs.gpodder.org/')
2113 def on_itemAbout_activate(self, widget, *args):
2114 dlg = gtk.AboutDialog()
2115 dlg.set_name(app_name.replace('p', 'P')) # gpodder->gPodder
2116 dlg.set_version( app_version)
2117 dlg.set_copyright( app_copyright)
2118 dlg.set_website( app_website)
2119 dlg.set_translator_credits( _('translator-credits'))
2120 dlg.connect( 'response', lambda dlg, response: dlg.destroy())
2122 if gpodder.interface == gpodder.GUI:
2123 # For the "GUI" version, we add some more
2124 # items to the about dialog (credits and logo)
2125 dlg.set_authors(app_authors)
2126 try:
2127 dlg.set_logo(gtk.gdk.pixbuf_new_from_file(scalable_dir))
2128 except:
2129 pass
2131 dlg.run()
2133 def on_wNotebook_switch_page(self, widget, *args):
2134 page_num = args[1]
2135 if gpodder.interface == gpodder.MAEMO:
2136 page = self.wNotebook.get_nth_page(page_num)
2137 tab_label = self.wNotebook.get_tab_label(page).get_text()
2138 if page_num == 0 and self.active_channel is not None:
2139 self.set_title(self.active_channel.title)
2140 else:
2141 self.set_title(tab_label)
2142 if page_num == 0:
2143 self.play_or_download()
2144 else:
2145 self.toolDownload.set_sensitive( False)
2146 self.toolPlay.set_sensitive( False)
2147 self.toolTransfer.set_sensitive( False)
2148 self.toolCancel.set_sensitive( services.download_status_manager.has_items())
2150 def on_treeChannels_row_activated(self, widget, *args):
2151 self.on_itemEditChannel_activate( self.treeChannels)
2153 def on_treeChannels_cursor_changed(self, widget, *args):
2154 ( model, iter ) = self.treeChannels.get_selection().get_selected()
2156 if model is not None and iter != None:
2157 id = model.get_path( iter)[0]
2158 self.active_channel = self.channels[id]
2160 if gpodder.interface == gpodder.MAEMO:
2161 self.set_title(self.active_channel.title)
2162 self.itemEditChannel.show_all()
2163 self.itemRemoveChannel.show_all()
2164 else:
2165 self.active_channel = None
2166 self.itemEditChannel.hide_all()
2167 self.itemRemoveChannel.hide_all()
2169 self.updateTreeView()
2171 def on_entryAddChannel_changed(self, widget, *args):
2172 active = self.entryAddChannel.get_text() not in ('', self.ENTER_URL_TEXT)
2173 self.btnAddChannel.set_sensitive( active)
2175 def on_btnAddChannel_clicked(self, widget, *args):
2176 url = self.entryAddChannel.get_text()
2177 self.entryAddChannel.set_text('')
2178 self.add_new_channel( url)
2180 def on_btnEditChannel_clicked(self, widget, *args):
2181 self.on_itemEditChannel_activate( widget, args)
2183 def on_treeAvailable_row_activated(self, widget, path=None, view_column=None):
2185 What this function does depends on from which widget it is called.
2186 It gets the selected episodes of the current podcast and runs one
2187 of the following actions on them:
2189 * Transfer (to MP3 player, iPod, etc..)
2190 * Playback/open files
2191 * Show the episode info dialog
2192 * Download episodes
2194 try:
2195 selection = self.treeAvailable.get_selection()
2196 (model, paths) = selection.get_selected_rows()
2198 wname = widget.get_name()
2199 do_transfer = (wname in ('itemTransferSelected', 'toolTransfer'))
2200 do_playback = (wname in ('itemPlaySelected', 'itemOpenSelected', 'toolPlay'))
2201 do_epdialog = (wname in ('treeAvailable', 'item_episode_details'))
2203 episodes = []
2204 for path in paths:
2205 it = model.get_iter(path)
2206 url = model.get_value(it, 0)
2207 episode = self.active_channel.find_episode(url)
2208 episodes.append(episode)
2210 if len(episodes) == 0:
2211 log('No episodes selected', sender=self)
2213 if do_transfer:
2214 self.on_sync_to_ipod_activate(widget, episodes)
2215 elif do_playback:
2216 for episode in episodes:
2217 # Make sure to mark the episode as downloaded
2218 if os.path.exists(episode.local_filename()):
2219 episode.channel.addDownloadedItem(episode)
2220 self.playback_episode(episode)
2221 elif do_epdialog:
2222 play_callback = lambda: self.playback_episode(episode)
2223 download_callback = lambda: self.download_episode_list([episode])
2224 gPodderEpisode(episode=episode, download_callback=download_callback, play_callback=play_callback)
2225 else:
2226 self.download_episode_list(episodes)
2227 except:
2228 log('Error in on_treeAvailable_row_activated', traceback=True, sender=self)
2230 def on_treeAvailable_button_release_event(self, widget, *args):
2231 self.play_or_download()
2233 def auto_update_procedure(self, first_run=False):
2234 log('auto_update_procedure() got called', sender=self)
2235 if not first_run and gl.config.auto_update_feeds and self.minimized:
2236 self.update_feed_cache(force_update=True)
2238 next_update = 60*1000*gl.config.auto_update_frequency
2239 gobject.timeout_add(next_update, self.auto_update_procedure)
2241 def on_treeDownloads_row_activated(self, widget, *args):
2242 cancel_urls = []
2244 if self.wNotebook.get_current_page() > 0:
2245 # Use the download list treeview + model
2246 ( tree, column ) = ( self.treeDownloads, 3 )
2247 else:
2248 # Use the available podcasts treeview + model
2249 ( tree, column ) = ( self.treeAvailable, 0 )
2251 selection = tree.get_selection()
2252 (model, paths) = selection.get_selected_rows()
2253 for path in paths:
2254 url = model.get_value( model.get_iter( path), column)
2255 cancel_urls.append( url)
2257 if len( cancel_urls) == 0:
2258 log('Nothing selected.', sender = self)
2259 return
2261 if len( cancel_urls) == 1:
2262 title = _('Cancel download?')
2263 message = _("Cancelling this download will remove the partially downloaded file and stop the download.")
2264 else:
2265 title = _('Cancel downloads?')
2266 message = _("Cancelling the download will stop the %d selected downloads and remove partially downloaded files.") % selection.count_selected_rows()
2268 if self.show_confirmation( message, title):
2269 services.download_status_manager.start_batch_mode()
2270 for url in cancel_urls:
2271 services.download_status_manager.cancel_by_url( url)
2272 services.download_status_manager.end_batch_mode()
2274 def on_btnCancelDownloadStatus_clicked(self, widget, *args):
2275 self.on_treeDownloads_row_activated( widget, None)
2277 def on_btnCancelAll_clicked(self, widget, *args):
2278 self.treeDownloads.get_selection().select_all()
2279 self.on_treeDownloads_row_activated( self.toolCancel, None)
2280 self.treeDownloads.get_selection().unselect_all()
2282 def on_btnDownloadedDelete_clicked(self, widget, *args):
2283 if self.active_channel is None:
2284 return
2286 channel_url = self.active_channel.url
2287 selection = self.treeAvailable.get_selection()
2288 ( model, paths ) = selection.get_selected_rows()
2290 if selection.count_selected_rows() == 0:
2291 log( 'Nothing selected - will not remove any downloaded episode.')
2292 return
2294 if selection.count_selected_rows() == 1:
2295 episode_title = saxutils.escape(model.get_value(model.get_iter(paths[0]), 1))
2297 episode = db.load_episode(model.get_value(model.get_iter(paths[0]), 0))
2298 if episode['is_locked']:
2299 title = _('%s is locked') % episode_title
2300 message = _('You cannot delete this locked episode. You must unlock it before you can delete it.')
2301 self.notification(message, title)
2302 return
2304 title = _('Remove %s?') % episode_title
2305 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.")
2306 else:
2307 title = _('Remove %d episodes?') % selection.count_selected_rows()
2308 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.')
2310 locked_count = 0
2311 for path in paths:
2312 episode = db.load_episode(model.get_value(model.get_iter(path), 0))
2313 if episode['is_locked']:
2314 locked_count += 1
2316 if selection.count_selected_rows() == locked_count:
2317 title = _('Episodes are locked')
2318 message = _('The selected episodes are locked. Please unlock the episodes that you want to delete before trying to delete them.')
2319 self.notification(message, title)
2320 return
2321 elif locked_count > 0:
2322 title = _('Remove %d out of %d episodes?') % (selection.count_selected_rows() - locked_count, selection.count_selected_rows())
2323 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.')
2325 # if user confirms deletion, let's remove some stuff ;)
2326 if self.show_confirmation( message, title):
2327 try:
2328 # iterate over the selection, see also on_treeDownloads_row_activated
2329 for path in paths:
2330 url = model.get_value( model.get_iter( path), 0)
2331 self.active_channel.delete_episode_by_url( url)
2333 # now, clear local db cache so we can re-read it
2334 self.updateComboBox()
2335 except:
2336 log( 'Error while deleting (some) downloads.')
2338 # only delete partial files if we do not have any downloads in progress
2339 delete_partial = not services.download_status_manager.has_items()
2340 gl.clean_up_downloads(delete_partial)
2341 self.updateTreeView()
2343 def on_key_press(self, widget, event):
2344 # Allow tab switching with Ctrl + PgUp/PgDown
2345 if event.state & gtk.gdk.CONTROL_MASK:
2346 if event.keyval == gtk.keysyms.Page_Up:
2347 self.wNotebook.prev_page()
2348 return True
2349 elif event.keyval == gtk.keysyms.Page_Down:
2350 self.wNotebook.next_page()
2351 return True
2353 # After this code we only handle Maemo hardware keys,
2354 # so if we are not a Maemo app, we don't do anything
2355 if gpodder.interface != gpodder.MAEMO:
2356 return False
2358 if event.keyval == gtk.keysyms.F6:
2359 if self.fullscreen:
2360 self.window.unfullscreen()
2361 else:
2362 self.window.fullscreen()
2363 if event.keyval == gtk.keysyms.Escape:
2364 new_visibility = not self.vboxChannelNavigator.get_property('visible')
2365 self.vboxChannelNavigator.set_property('visible', new_visibility)
2366 self.column_size.set_visible(not new_visibility)
2367 self.column_released.set_visible(not new_visibility)
2369 diff = 0
2370 if event.keyval == gtk.keysyms.F7: #plus
2371 diff = 1
2372 elif event.keyval == gtk.keysyms.F8: #minus
2373 diff = -1
2375 if diff != 0:
2376 selection = self.treeChannels.get_selection()
2377 (model, iter) = selection.get_selected()
2378 selection.select_path(((model.get_path(iter)[0]+diff)%len(model),))
2379 self.on_treeChannels_cursor_changed(self.treeChannels)
2381 def window_state_event(self, widget, event):
2382 if event.new_window_state & gtk.gdk.WINDOW_STATE_FULLSCREEN:
2383 self.fullscreen = True
2384 else:
2385 self.fullscreen = False
2387 old_minimized = self.minimized
2389 self.minimized = bool(event.new_window_state & gtk.gdk.WINDOW_STATE_ICONIFIED)
2390 if gpodder.interface == gpodder.MAEMO:
2391 self.minimized = bool(event.new_window_state & gtk.gdk.WINDOW_STATE_WITHDRAWN)
2393 if old_minimized != self.minimized and self.tray_icon:
2394 self.gPodder.set_skip_taskbar_hint(self.minimized)
2395 elif not self.tray_icon:
2396 self.gPodder.set_skip_taskbar_hint(False)
2398 if gl.config.minimize_to_tray and self.tray_icon:
2399 self.tray_icon.set_visible(self.minimized)
2401 def uniconify_main_window(self):
2402 if self.minimized:
2403 self.gPodder.present()
2405 def iconify_main_window(self):
2406 if not self.minimized:
2407 self.gPodder.iconify()
2409 def update_podcasts_tab(self):
2410 if len(self.channels):
2411 self.label2.set_text(_('Podcasts (%d)') % len(self.channels))
2412 else:
2413 self.label2.set_text(_('Podcasts'))
2415 class gPodderChannel(GladeWidget):
2416 finger_friendly_widgets = ['btn_website', 'btnOK', 'channel_description']
2418 def new(self):
2419 global WEB_BROWSER_ICON
2420 self.changed = False
2421 self.image3167.set_property('icon-name', WEB_BROWSER_ICON)
2422 self.gPodderChannel.set_title( self.channel.title)
2423 self.entryTitle.set_text( self.channel.title)
2424 self.entryURL.set_text( self.channel.url)
2426 self.LabelDownloadTo.set_text( self.channel.save_dir)
2427 self.LabelWebsite.set_text( self.channel.link)
2429 self.cbNoSync.set_active( not self.channel.sync_to_devices)
2430 self.musicPlaylist.set_text(self.channel.device_playlist_name)
2431 if self.channel.username:
2432 self.FeedUsername.set_text( self.channel.username)
2433 if self.channel.password:
2434 self.FeedPassword.set_text( self.channel.password)
2436 services.cover_downloader.register('cover-available', self.cover_download_finished)
2437 services.cover_downloader.request_cover(self.channel)
2439 # Hide the website button if we don't have a valid URL
2440 if not self.channel.link:
2441 self.btn_website.hide_all()
2443 b = gtk.TextBuffer()
2444 b.set_text( self.channel.description)
2445 self.channel_description.set_buffer( b)
2447 #Add Drag and Drop Support
2448 flags = gtk.DEST_DEFAULT_ALL
2449 targets = [ ('text/uri-list', 0, 2), ('text/plain', 0, 4) ]
2450 actions = gtk.gdk.ACTION_DEFAULT | gtk.gdk.ACTION_COPY
2451 self.vboxCoverEditor.drag_dest_set( flags, targets, actions)
2452 self.vboxCoverEditor.connect( 'drag_data_received', self.drag_data_received)
2454 def on_btn_website_clicked(self, widget):
2455 util.open_website(self.channel.link)
2457 def on_btnDownloadCover_clicked(self, widget):
2458 if gpodder.interface == gpodder.GUI:
2459 dlg = gtk.FileChooserDialog(title=_('Select new podcast cover artwork'), parent=self.gPodderChannel, action=gtk.FILE_CHOOSER_ACTION_OPEN)
2460 dlg.add_button(gtk.STOCK_CANCEL, gtk.RESPONSE_CANCEL)
2461 dlg.add_button(gtk.STOCK_OPEN, gtk.RESPONSE_OK)
2462 elif gpodder.interface == gpodder.MAEMO:
2463 dlg = hildon.FileChooserDialog(self.gPodderChannel, gtk.FILE_CHOOSER_ACTION_OPEN)
2465 if dlg.run() == gtk.RESPONSE_OK:
2466 url = dlg.get_uri()
2467 services.cover_downloader.replace_cover(self.channel, url)
2469 dlg.destroy()
2471 def on_btnClearCover_clicked(self, widget):
2472 services.cover_downloader.replace_cover(self.channel)
2474 def cover_download_finished(self, channel_url, pixbuf):
2475 if pixbuf is not None:
2476 self.imgCover.set_from_pixbuf(pixbuf)
2477 self.gPodderChannel.show()
2479 def drag_data_received( self, widget, content, x, y, sel, ttype, time):
2480 files = sel.data.strip().split('\n')
2481 if len(files) != 1:
2482 self.show_message( _('You can only drop a single image or URL here.'), _('Drag and drop'))
2483 return
2485 file = files[0]
2487 if file.startswith('file://') or file.startswith('http://'):
2488 services.cover_downloader.replace_cover(self.channel, file)
2489 return
2491 self.show_message( _('You can only drop local files and http:// URLs here.'), _('Drag and drop'))
2493 def on_gPodderChannel_destroy(self, widget, *args):
2494 services.cover_downloader.unregister('cover-available', self.cover_download_finished)
2496 def on_btnOK_clicked(self, widget, *args):
2497 entered_url = self.entryURL.get_text()
2498 channel_url = self.channel.url
2500 if entered_url != channel_url:
2501 if self.show_confirmation(_('Do you really want to move this podcast to <b>%s</b>?') % (saxutils.escape(entered_url),), _('Really change URL?')):
2502 if hasattr(self, 'callback_change_url'):
2503 self.gPodderChannel.hide_all()
2504 self.callback_change_url(channel_url, entered_url)
2506 self.channel.sync_to_devices = not self.cbNoSync.get_active()
2507 self.channel.device_playlist_name = self.musicPlaylist.get_text()
2508 self.channel.set_custom_title( self.entryTitle.get_text())
2509 self.channel.username = self.FeedUsername.get_text().strip()
2510 self.channel.password = self.FeedPassword.get_text()
2511 self.channel.save()
2513 self.gPodderChannel.destroy()
2514 self.callback_closed()
2516 class gPodderAddPodcastDialog(GladeWidget):
2517 finger_friendly_widgets = ['btn_close', 'btn_add']
2519 def new(self):
2520 if not hasattr(self, 'url_callback'):
2521 log('No url callback set', sender=self)
2522 self.url_callback = None
2523 if hasattr(self, 'custom_label'):
2524 self.label_add.set_text(self.custom_label)
2525 if hasattr(self, 'custom_title'):
2526 self.gPodderAddPodcastDialog.set_title(self.custom_title)
2528 def on_btn_close_clicked(self, widget):
2529 self.gPodderAddPodcastDialog.destroy()
2531 def on_entry_url_changed(self, widget):
2532 self.btn_add.set_sensitive(self.entry_url.get_text().strip() != '')
2534 def on_btn_add_clicked(self, widget):
2535 url = self.entry_url.get_text()
2536 self.on_btn_close_clicked(widget)
2537 if self.url_callback is not None:
2538 self.url_callback(url)
2541 class gPodderMaemoPreferences(GladeWidget):
2542 finger_friendly_widgets = ['btn_close', 'label128', 'label129', 'btn_advanced']
2544 def new(self):
2545 gl.config.connect_gtk_togglebutton('update_on_startup', self.update_on_startup)
2546 gl.config.connect_gtk_togglebutton('display_tray_icon', self.show_tray_icon)
2547 gl.config.connect_gtk_togglebutton('enable_notifications', self.show_notifications)
2548 gl.config.connect_gtk_togglebutton('on_quit_ask', self.on_quit_ask)
2550 self.restart_required = False
2551 self.show_tray_icon.connect('clicked', self.on_restart_required)
2552 self.show_notifications.connect('clicked', self.on_restart_required)
2554 def on_restart_required(self, widget):
2555 self.restart_required = True
2557 def on_btn_advanced_clicked(self, widget):
2558 self.gPodderMaemoPreferences.destroy()
2559 gPodderConfigEditor()
2561 def on_btn_close_clicked(self, widget):
2562 self.gPodderMaemoPreferences.destroy()
2563 if self.restart_required:
2564 self.show_message(_('Please restart gPodder for the changes to take effect.'))
2567 class gPodderProperties(GladeWidget):
2568 def new(self):
2569 if not hasattr( self, 'callback_finished'):
2570 self.callback_finished = None
2572 if gpodder.interface == gpodder.MAEMO:
2573 self.table13.hide_all() # bluetooth
2574 self.table5.hide_all() # player
2575 self.table6.hide_all() # bittorrent
2576 self.gPodderProperties.fullscreen()
2578 gl.config.connect_gtk_editable( 'http_proxy', self.httpProxy)
2579 gl.config.connect_gtk_editable( 'ftp_proxy', self.ftpProxy)
2580 gl.config.connect_gtk_editable( 'player', self.openApp)
2581 gl.config.connect_gtk_editable('videoplayer', self.openVideoApp)
2582 gl.config.connect_gtk_editable( 'custom_sync_name', self.entryCustomSyncName)
2583 gl.config.connect_gtk_togglebutton( 'custom_sync_name_enabled', self.cbCustomSyncName)
2584 gl.config.connect_gtk_togglebutton( 'auto_download_when_minimized', self.downloadnew)
2585 gl.config.connect_gtk_togglebutton( 'use_gnome_bittorrent', self.radio_gnome_bittorrent)
2586 gl.config.connect_gtk_togglebutton( 'update_on_startup', self.updateonstartup)
2587 gl.config.connect_gtk_togglebutton( 'only_sync_not_played', self.only_sync_not_played)
2588 gl.config.connect_gtk_togglebutton( 'fssync_channel_subfolders', self.cbChannelSubfolder)
2589 gl.config.connect_gtk_togglebutton( 'on_sync_mark_played', self.on_sync_mark_played)
2590 gl.config.connect_gtk_togglebutton( 'on_sync_delete', self.on_sync_delete)
2591 gl.config.connect_gtk_togglebutton( 'proxy_use_environment', self.cbEnvironmentVariables)
2592 gl.config.connect_gtk_filechooser( 'bittorrent_dir', self.chooserBitTorrentTo)
2593 gl.config.connect_gtk_spinbutton('episode_old_age', self.episode_old_age)
2594 gl.config.connect_gtk_togglebutton('auto_remove_old_episodes', self.auto_remove_old_episodes)
2595 gl.config.connect_gtk_togglebutton('auto_update_feeds', self.auto_update_feeds)
2596 gl.config.connect_gtk_spinbutton('auto_update_frequency', self.auto_update_frequency)
2597 gl.config.connect_gtk_togglebutton('display_tray_icon', self.display_tray_icon)
2598 gl.config.connect_gtk_togglebutton('minimize_to_tray', self.minimize_to_tray)
2599 gl.config.connect_gtk_togglebutton('enable_notifications', self.enable_notifications)
2600 gl.config.connect_gtk_togglebutton('start_iconified', self.start_iconified)
2601 gl.config.connect_gtk_togglebutton('bluetooth_enabled', self.bluetooth_enabled)
2602 gl.config.connect_gtk_togglebutton('bluetooth_ask_always', self.bluetooth_ask_always)
2603 gl.config.connect_gtk_togglebutton('bluetooth_ask_never', self.bluetooth_ask_never)
2604 gl.config.connect_gtk_togglebutton('bluetooth_use_converter', self.bluetooth_use_converter)
2605 gl.config.connect_gtk_filechooser( 'bluetooth_converter', self.bluetooth_converter, is_for_files=True)
2606 gl.config.connect_gtk_togglebutton('ipod_write_gtkpod_extended', self.ipod_write_gtkpod_extended)
2607 gl.config.connect_gtk_togglebutton('mp3_player_delete_played', self.delete_episodes_marked_played)
2609 self.enable_notifications.set_sensitive(self.display_tray_icon.get_active())
2610 self.minimize_to_tray.set_sensitive(self.display_tray_icon.get_active())
2612 self.entryCustomSyncName.set_sensitive( self.cbCustomSyncName.get_active())
2614 self.radio_gnome_bittorrent.set_active(gl.config.use_gnome_bittorrent)
2615 self.radio_copy_torrents.set_active(not gl.config.use_gnome_bittorrent)
2617 self.iPodMountpoint.set_label( gl.config.ipod_mount)
2618 self.filesystemMountpoint.set_label( gl.config.mp3_player_folder)
2619 self.bluetooth_device_name.set_markup('<b>%s</b>'%gl.config.bluetooth_device_name)
2620 self.chooserDownloadTo.set_current_folder(gl.downloaddir)
2622 self.on_sync_delete.set_sensitive(not self.delete_episodes_marked_played.get_active())
2623 self.on_sync_mark_played.set_sensitive(not self.delete_episodes_marked_played.get_active())
2625 if tagging_supported():
2626 gl.config.connect_gtk_togglebutton( 'update_tags', self.updatetags)
2627 else:
2628 self.updatetags.set_sensitive( False)
2629 new_label = '%s (%s)' % ( self.updatetags.get_label(), _('needs python-eyed3') )
2630 self.updatetags.set_label( new_label)
2632 # device type
2633 self.comboboxDeviceType.set_active( 0)
2634 if gl.config.device_type == 'ipod':
2635 self.comboboxDeviceType.set_active( 1)
2636 elif gl.config.device_type == 'filesystem':
2637 self.comboboxDeviceType.set_active( 2)
2638 elif gl.config.device_type == 'mtp':
2639 self.comboboxDeviceType.set_active( 3)
2641 # setup cell renderers
2642 cellrenderer = gtk.CellRendererPixbuf()
2643 self.comboAudioPlayerApp.pack_start(cellrenderer, False)
2644 self.comboAudioPlayerApp.add_attribute(cellrenderer, 'pixbuf', 2)
2645 cellrenderer = gtk.CellRendererText()
2646 self.comboAudioPlayerApp.pack_start(cellrenderer, True)
2647 self.comboAudioPlayerApp.add_attribute(cellrenderer, 'markup', 0)
2649 cellrenderer = gtk.CellRendererPixbuf()
2650 self.comboVideoPlayerApp.pack_start(cellrenderer, False)
2651 self.comboVideoPlayerApp.add_attribute(cellrenderer, 'pixbuf', 2)
2652 cellrenderer = gtk.CellRendererText()
2653 self.comboVideoPlayerApp.pack_start(cellrenderer, True)
2654 self.comboVideoPlayerApp.add_attribute(cellrenderer, 'markup', 0)
2656 if not hasattr(self, 'user_apps_reader'):
2657 self.user_apps_reader = UserAppsReader(['audio', 'video'])
2659 if gpodder.interface == gpodder.GUI:
2660 self.user_apps_reader.read()
2662 self.comboAudioPlayerApp.set_model(self.user_apps_reader.get_applications_as_model('audio'))
2663 index = self.find_active_audio_app()
2664 self.comboAudioPlayerApp.set_active(index)
2665 self.comboVideoPlayerApp.set_model(self.user_apps_reader.get_applications_as_model('video'))
2666 index = self.find_active_video_app()
2667 self.comboVideoPlayerApp.set_active(index)
2669 self.ipodIcon.set_from_icon_name( 'gnome-dev-ipod', gtk.ICON_SIZE_BUTTON)
2671 def update_mountpoint( self, ipod):
2672 if ipod is None or ipod.mount_point is None:
2673 self.iPodMountpoint.set_label( '')
2674 else:
2675 self.iPodMountpoint.set_label( ipod.mount_point)
2677 def on_bluetooth_select_device_clicked(self, widget):
2678 # Stupid GTK doesn't provide us with a method to directly
2679 # edit the text of a gtk.Button without "destroying" the
2680 # image on it, so we dig into the button's widget tree and
2681 # get the gtk.Image and gtk.Label and edit the label directly.
2682 alignment = self.bluetooth_select_device.get_child()
2683 hbox = alignment.get_child()
2684 (image, label) = hbox.get_children()
2686 old_text = label.get_text()
2687 label.set_text(_('Searching...'))
2688 self.bluetooth_select_device.set_sensitive(False)
2689 while gtk.events_pending():
2690 gtk.main_iteration(False)
2692 # FIXME: Make bluetooth device discovery threaded, so
2693 # the GUI doesn't freeze while we are searching for devices
2694 found = False
2695 for name, address in util.discover_bluetooth_devices():
2696 if self.show_confirmation('Use this device as your bluetooth device?', name):
2697 gl.config.bluetooth_device_name = name
2698 gl.config.bluetooth_device_address = address
2699 self.bluetooth_device_name.set_markup('<b>%s</b>'%gl.config.bluetooth_device_name)
2700 found = True
2701 break
2702 if not found:
2703 self.show_message('No more devices found', 'Scan finished')
2704 self.bluetooth_select_device.set_sensitive(True)
2705 label.set_text(old_text)
2707 def find_active_audio_app(self):
2708 model = self.comboAudioPlayerApp.get_model()
2709 iter = model.get_iter_first()
2710 index = 0
2711 while iter is not None:
2712 command = model.get_value(iter, 1)
2713 if command == self.openApp.get_text():
2714 return index
2715 iter = model.iter_next(iter)
2716 index += 1
2717 # return last item = custom command
2718 return index-1
2720 def find_active_video_app( self):
2721 model = self.comboVideoPlayerApp.get_model()
2722 iter = model.get_iter_first()
2723 index = 0
2724 while iter is not None:
2725 command = model.get_value(iter, 1)
2726 if command == self.openVideoApp.get_text():
2727 return index
2728 iter = model.iter_next(iter)
2729 index += 1
2730 # return last item = custom command
2731 return index-1
2733 def set_download_dir( self, new_download_dir, event = None):
2734 gl.downloaddir = self.chooserDownloadTo.get_filename()
2735 if gl.downloaddir != self.chooserDownloadTo.get_filename():
2736 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'))
2738 if event:
2739 event.set()
2741 def on_auto_update_feeds_toggled( self, widget, *args):
2742 self.auto_update_frequency.set_sensitive(widget.get_active())
2744 def on_display_tray_icon_toggled( self, widget, *args):
2745 self.enable_notifications.set_sensitive(widget.get_active())
2746 self.minimize_to_tray.set_sensitive(widget.get_active())
2748 def on_cbCustomSyncName_toggled( self, widget, *args):
2749 self.entryCustomSyncName.set_sensitive( widget.get_active())
2751 def on_only_sync_not_played_toggled( self, widget, *args):
2752 self.delete_episodes_marked_played.set_sensitive( widget.get_active())
2753 if not widget.get_active():
2754 self.delete_episodes_marked_played.set_active(False)
2756 def on_delete_episodes_marked_played_toggled( self, widget, *args):
2757 if widget.get_active() and self.only_sync_not_played.get_active():
2758 self.on_sync_leave.set_active(True)
2759 self.on_sync_delete.set_sensitive(not widget.get_active())
2760 self.on_sync_mark_played.set_sensitive(not widget.get_active())
2762 def on_btnCustomSyncNameHelp_clicked( self, widget):
2763 examples = [
2764 '<i>{episode.title}</i> -&gt; <b>Interview with RMS</b>',
2765 '<i>{episode.basename}</i> -&gt; <b>70908-interview-rms</b>',
2766 '<i>{episode.published}</i> -&gt; <b>20070908</b>'
2769 info = [
2770 _('You can specify a custom format string for the file names on your MP3 player here.'),
2771 _('The format string will be used to generate a file name on your device. The file extension (e.g. ".mp3") will be added automatically.'),
2772 '\n'.join( [ ' %s' % s for s in examples ])
2775 self.show_message( '\n\n'.join( info), _('Custom format strings'))
2777 def on_gPodderProperties_destroy(self, widget, *args):
2778 self.on_btnOK_clicked( widget, *args)
2780 def on_btnConfigEditor_clicked(self, widget, *args):
2781 self.on_btnOK_clicked(widget, *args)
2782 gPodderConfigEditor()
2784 def on_comboAudioPlayerApp_changed(self, widget, *args):
2785 # find out which one
2786 iter = self.comboAudioPlayerApp.get_active_iter()
2787 model = self.comboAudioPlayerApp.get_model()
2788 command = model.get_value( iter, 1)
2789 if command == '':
2790 self.openApp.set_sensitive( True)
2791 self.openApp.show()
2792 self.labelCustomCommand.show()
2793 else:
2794 self.openApp.set_text( command)
2795 self.openApp.set_sensitive( False)
2796 self.openApp.hide()
2797 self.labelCustomCommand.hide()
2799 def on_comboVideoPlayerApp_changed(self, widget, *args):
2800 # find out which one
2801 iter = self.comboVideoPlayerApp.get_active_iter()
2802 model = self.comboVideoPlayerApp.get_model()
2803 command = model.get_value(iter, 1)
2804 if command == '':
2805 self.openVideoApp.set_sensitive(True)
2806 self.openVideoApp.show()
2807 self.label115.show()
2808 else:
2809 self.openVideoApp.set_text(command)
2810 self.openVideoApp.set_sensitive(False)
2811 self.openVideoApp.hide()
2812 self.label115.hide()
2814 def on_cbEnvironmentVariables_toggled(self, widget, *args):
2815 sens = not self.cbEnvironmentVariables.get_active()
2816 self.httpProxy.set_sensitive( sens)
2817 self.ftpProxy.set_sensitive( sens)
2819 def on_comboboxDeviceType_changed(self, widget, *args):
2820 active_item = self.comboboxDeviceType.get_active()
2822 # None
2823 sync_widgets = ( self.only_sync_not_played, self.labelSyncOptions,
2824 self.imageSyncOptions, self. separatorSyncOptions,
2825 self.on_sync_mark_played, self.on_sync_delete,
2826 self.on_sync_leave, self.label_after_sync, self.delete_episodes_marked_played)
2827 for widget in sync_widgets:
2828 if active_item == 0:
2829 widget.hide_all()
2830 else:
2831 widget.show_all()
2833 # iPod
2834 ipod_widgets = (self.ipodLabel, self.btn_iPodMountpoint,
2835 self.ipod_write_gtkpod_extended)
2836 for widget in ipod_widgets:
2837 if active_item == 1:
2838 widget.show_all()
2839 else:
2840 widget.hide_all()
2842 # filesystem-based MP3 player
2843 fs_widgets = ( self.filesystemLabel, self.btn_filesystemMountpoint,
2844 self.cbChannelSubfolder, self.cbCustomSyncName,
2845 self.entryCustomSyncName, self.btnCustomSyncNameHelp )
2846 for widget in fs_widgets:
2847 if active_item == 2:
2848 widget.show_all()
2849 else:
2850 widget.hide_all()
2852 def on_btn_iPodMountpoint_clicked(self, widget, *args):
2853 fs = gtk.FileChooserDialog( title = _('Select iPod mountpoint'), action = gtk.FILE_CHOOSER_ACTION_SELECT_FOLDER)
2854 fs.add_button( gtk.STOCK_CANCEL, gtk.RESPONSE_CANCEL)
2855 fs.add_button( gtk.STOCK_OPEN, gtk.RESPONSE_OK)
2856 fs.set_current_folder(self.iPodMountpoint.get_label())
2857 if fs.run() == gtk.RESPONSE_OK:
2858 self.iPodMountpoint.set_label( fs.get_filename())
2859 fs.destroy()
2861 def on_btn_FilesystemMountpoint_clicked(self, widget, *args):
2862 fs = gtk.FileChooserDialog( title = _('Select folder for MP3 player'), action = gtk.FILE_CHOOSER_ACTION_SELECT_FOLDER)
2863 fs.add_button( gtk.STOCK_CANCEL, gtk.RESPONSE_CANCEL)
2864 fs.add_button( gtk.STOCK_OPEN, gtk.RESPONSE_OK)
2865 fs.set_current_folder(self.filesystemMountpoint.get_label())
2866 if fs.run() == gtk.RESPONSE_OK:
2867 self.filesystemMountpoint.set_label( fs.get_filename())
2868 fs.destroy()
2870 def on_btnOK_clicked(self, widget, *args):
2871 gl.config.ipod_mount = self.iPodMountpoint.get_label()
2872 gl.config.mp3_player_folder = self.filesystemMountpoint.get_label()
2874 if gl.downloaddir != self.chooserDownloadTo.get_filename():
2875 new_download_dir = self.chooserDownloadTo.get_filename()
2876 download_dir_size = util.calculate_size( gl.downloaddir)
2877 download_dir_size_string = gl.format_filesize( download_dir_size)
2878 event = Event()
2880 dlg = gtk.Dialog( _('Moving downloads folder'), self.gPodderProperties)
2881 dlg.vbox.set_spacing( 5)
2882 dlg.set_border_width( 5)
2884 label = gtk.Label()
2885 label.set_line_wrap( True)
2886 label.set_markup( _('Moving downloads from <b>%s</b> to <b>%s</b>...') % ( saxutils.escape( gl.downloaddir), saxutils.escape( new_download_dir), ))
2887 myprogressbar = gtk.ProgressBar()
2889 # put it all together
2890 dlg.vbox.pack_start( label)
2891 dlg.vbox.pack_end( myprogressbar)
2893 # switch windows
2894 dlg.show_all()
2895 self.gPodderProperties.hide_all()
2897 # hide action area and separator line
2898 dlg.action_area.hide()
2899 dlg.set_has_separator( False)
2901 args = ( new_download_dir, event, )
2903 thread = Thread( target = self.set_download_dir, args = args)
2904 thread.start()
2906 while not event.isSet():
2907 try:
2908 new_download_dir_size = util.calculate_size( new_download_dir)
2909 except:
2910 new_download_dir_size = 0
2911 if download_dir_size > 0:
2912 fract = (1.00*new_download_dir_size) / (1.00*download_dir_size)
2913 else:
2914 fract = 0.0
2915 if fract < 0.99:
2916 myprogressbar.set_text( _('%s of %s') % ( gl.format_filesize( new_download_dir_size), download_dir_size_string, ))
2917 else:
2918 myprogressbar.set_text( _('Finishing... please wait.'))
2919 myprogressbar.set_fraction(max(0.0,min(1.0,fract)))
2920 event.wait( 0.1)
2921 while gtk.events_pending():
2922 gtk.main_iteration( False)
2924 dlg.destroy()
2926 device_type = self.comboboxDeviceType.get_active()
2927 if device_type == 0:
2928 gl.config.device_type = 'none'
2929 elif device_type == 1:
2930 gl.config.device_type = 'ipod'
2931 elif device_type == 2:
2932 gl.config.device_type = 'filesystem'
2933 elif device_type == 3:
2934 gl.config.device_type = 'mtp'
2935 self.gPodderProperties.destroy()
2936 if self.callback_finished:
2937 self.callback_finished()
2940 class gPodderEpisode(GladeWidget):
2941 finger_friendly_widgets = ['episode_description', 'btnCloseWindow', 'btnDownload',
2942 'btnCancel', 'btnSaveFile', 'btnPlay', 'btn_website']
2944 def new(self):
2945 global WEB_BROWSER_ICON
2946 self.image3166.set_property('icon-name', WEB_BROWSER_ICON)
2947 services.download_status_manager.register( 'list-changed', self.on_download_status_changed)
2948 services.download_status_manager.register( 'progress-detail', self.on_download_status_progress)
2950 self.episode_title.set_markup( '<span weight="bold" size="larger">%s</span>' % saxutils.escape( self.episode.title))
2952 if gpodder.interface == gpodder.MAEMO:
2953 # Hide the advanced prefs expander
2954 self.expander1.hide_all()
2956 b = gtk.TextBuffer()
2957 b.set_text( strip( self.episode.description))
2958 self.episode_description.set_buffer( b)
2960 self.gPodderEpisode.set_title( self.episode.title)
2961 self.LabelDownloadLink.set_text( self.episode.url)
2962 self.LabelWebsiteLink.set_text( self.episode.link)
2963 self.labelPubDate.set_text(self.episode.cute_pubdate())
2965 # Hide the "Go to website" button if we don't have a valid URL
2966 if self.episode.link == self.episode.url or not self.episode.link:
2967 self.btn_website.hide_all()
2969 self.channel_title.set_markup(_('<i>from %s</i>') % saxutils.escape(self.episode.channel.title))
2971 self.hide_show_widgets()
2972 services.download_status_manager.request_progress_detail( self.episode.url)
2974 def on_btnCancel_clicked( self, widget):
2975 services.download_status_manager.cancel_by_url( self.episode.url)
2977 def on_gPodderEpisode_destroy( self, widget):
2978 services.download_status_manager.unregister( 'list-changed', self.on_download_status_changed)
2979 services.download_status_manager.unregister( 'progress-detail', self.on_download_status_progress)
2981 def on_download_status_changed( self):
2982 self.hide_show_widgets()
2984 def on_btn_website_clicked(self, widget):
2985 util.open_website(self.episode.link)
2987 def on_download_status_progress( self, url, progress, speed):
2988 if url == self.episode.url:
2989 progress = float(min(100.0,max(0.0,progress)))
2990 self.progress_bar.set_fraction(progress/100.0)
2991 self.progress_bar.set_text( 'Downloading: %d%% (%s)' % ( progress, speed, ))
2993 def hide_show_widgets( self):
2994 is_downloading = services.download_status_manager.is_download_in_progress( self.episode.url)
2995 if is_downloading:
2996 self.progress_bar.show_all()
2997 self.btnCancel.show_all()
2998 self.btnPlay.hide_all()
2999 self.btnSaveFile.hide_all()
3000 self.btnDownload.hide_all()
3001 else:
3002 self.progress_bar.hide_all()
3003 self.btnCancel.hide_all()
3004 if os.path.exists( self.episode.local_filename()):
3005 if self.episode.file_type() in ('audio', 'video'):
3006 self.btnPlay.set_label(gtk.STOCK_MEDIA_PLAY)
3007 else:
3008 self.btnPlay.set_label(gtk.STOCK_OPEN)
3009 self.btnPlay.set_use_stock(True)
3010 self.btnPlay.show_all()
3011 self.btnSaveFile.show_all()
3012 self.btnDownload.hide_all()
3013 else:
3014 self.btnPlay.hide_all()
3015 self.btnSaveFile.hide_all()
3016 self.btnDownload.show_all()
3018 def on_btnCloseWindow_clicked(self, widget, *args):
3019 self.gPodderEpisode.destroy()
3021 def on_btnDownload_clicked(self, widget, *args):
3022 if self.download_callback:
3023 self.download_callback()
3025 def on_btnPlay_clicked(self, widget, *args):
3026 if self.play_callback:
3027 self.play_callback()
3029 self.gPodderEpisode.destroy()
3031 def on_btnSaveFile_clicked(self, widget, *args):
3032 self.show_copy_dialog( src_filename = self.episode.local_filename(), dst_filename = self.episode.sync_filename())
3035 class gPodderSync(GladeWidget):
3036 def new(self):
3037 util.idle_add(self.imageSync.set_from_icon_name, 'gnome-dev-ipod', gtk.ICON_SIZE_DIALOG)
3039 self.device.register('progress', self.on_progress)
3040 self.device.register('sub-progress', self.on_sub_progress)
3041 self.device.register('status', self.on_status)
3042 self.device.register('done', self.on_done)
3044 def on_progress(self, pos, max, text=None):
3045 if text is None:
3046 text = _('%d of %d done') % (pos, max)
3047 util.idle_add(self.progressbar.set_fraction, float(pos)/float(max))
3048 util.idle_add(self.progressbar.set_text, text)
3050 def on_sub_progress(self, percentage):
3051 util.idle_add(self.progressbar.set_text, _('Processing (%d%%)') % (percentage))
3053 def on_status(self, status):
3054 util.idle_add(self.status_label.set_markup, '<i>%s</i>' % saxutils.escape(status))
3056 def on_done(self):
3057 util.idle_add(self.gPodderSync.destroy)
3058 if not self.gPodder.minimized:
3059 util.idle_add(self.notification, _('Your device has been updated by gPodder.'), _('Operation finished'))
3061 def on_gPodderSync_destroy(self, widget, *args):
3062 self.device.unregister('progress', self.on_progress)
3063 self.device.unregister('sub-progress', self.on_sub_progress)
3064 self.device.unregister('status', self.on_status)
3065 self.device.unregister('done', self.on_done)
3066 self.device.cancel()
3068 def on_cancel_button_clicked(self, widget, *args):
3069 self.device.cancel()
3072 class gPodderOpmlLister(GladeWidget):
3073 finger_friendly_widgets = ['btnDownloadOpml', 'btnCancel', 'btnOK', 'treeviewChannelChooser']
3075 def new(self):
3076 # initiate channels list
3077 self.channels = []
3078 self.callback_for_channel = None
3079 self.callback_finished = None
3081 if hasattr(self, 'custom_title'):
3082 self.gPodderOpmlLister.set_title(self.custom_title)
3083 if hasattr(self, 'hide_url_entry'):
3084 self.hbox25.hide_all()
3086 togglecell = gtk.CellRendererToggle()
3087 togglecell.set_property( 'activatable', True)
3088 togglecell.connect( 'toggled', self.callback_edited)
3089 togglecolumn = gtk.TreeViewColumn( '', togglecell, active=0)
3091 titlecell = gtk.CellRendererText()
3092 titlecell.set_property('ellipsize', pango.ELLIPSIZE_END)
3093 titlecolumn = gtk.TreeViewColumn(_('Podcast'), titlecell, markup=1)
3095 for itemcolumn in ( togglecolumn, titlecolumn ):
3096 self.treeviewChannelChooser.append_column( itemcolumn)
3098 def callback_edited( self, cell, path):
3099 model = self.treeviewChannelChooser.get_model()
3101 url = model[path][2]
3103 model[path][0] = not model[path][0]
3104 if model[path][0]:
3105 self.channels.append( url)
3106 else:
3107 self.channels.remove( url)
3109 self.btnOK.set_sensitive( bool(len(self.channels)))
3111 def thread_finished(self, model):
3112 self.treeviewChannelChooser.set_model(model)
3113 self.btnDownloadOpml.set_sensitive(True)
3114 self.entryURL.set_sensitive(True)
3115 self.treeviewChannelChooser.set_sensitive(True)
3116 self.channels = []
3118 def thread_func(self):
3119 url = self.entryURL.get_text()
3120 importer = opml.Importer(url)
3121 model = importer.get_model()
3122 if len(model) == 0:
3123 self.notification(_('The specified URL does not provide any valid OPML podcast items.'), _('No feeds found'))
3124 util.idle_add(self.thread_finished, model)
3126 def get_channels_from_url( self, url, callback_for_channel = None, callback_finished = None):
3127 if callback_for_channel:
3128 self.callback_for_channel = callback_for_channel
3129 if callback_finished:
3130 self.callback_finished = callback_finished
3131 self.entryURL.set_text( url)
3132 self.btnDownloadOpml.set_sensitive( False)
3133 self.entryURL.set_sensitive( False)
3134 self.btnOK.set_sensitive( False)
3135 self.treeviewChannelChooser.set_sensitive( False)
3136 Thread( target = self.thread_func).start()
3138 def select_all( self, value ):
3139 self.channels = []
3140 for row in self.treeviewChannelChooser.get_model():
3141 row[0] = value
3142 if value:
3143 self.channels.append(row[2])
3144 self.btnOK.set_sensitive(bool(len(self.channels)))
3146 def on_gPodderOpmlLister_destroy(self, widget, *args):
3147 pass
3149 def on_btnDownloadOpml_clicked(self, widget, *args):
3150 self.get_channels_from_url( self.entryURL.get_text())
3152 def on_btnSelectAll_clicked(self, widget, *args):
3153 self.select_all(True)
3155 def on_btnSelectNone_clicked(self, widget, *args):
3156 self.select_all(False)
3158 def on_btnOK_clicked(self, widget, *args):
3159 self.gPodderOpmlLister.destroy()
3161 # add channels that have been selected
3162 for url in self.channels:
3163 if self.callback_for_channel:
3164 self.callback_for_channel( url)
3166 if self.callback_finished:
3167 util.idle_add(self.callback_finished)
3169 def on_btnCancel_clicked(self, widget, *args):
3170 self.gPodderOpmlLister.destroy()
3173 class gPodderEpisodeSelector( GladeWidget):
3174 """Episode selection dialog
3176 Optional keyword arguments that modify the behaviour of this dialog:
3178 - callback: Function that takes 1 parameter which is a list of
3179 the selected episodes (or empty list when none selected)
3180 - episodes: List of episodes that are presented for selection
3181 - selected: (optional) List of boolean variables that define the
3182 default checked state for the given episodes
3183 - selected_default: (optional) The default boolean value for the
3184 checked state if no other value is set
3185 (default is False)
3186 - columns: List of (name, sort_name, sort_type, caption) pairs for the
3187 columns, the name is the attribute name of the episode to be
3188 read from each episode object. The sort name is the
3189 attribute name of the episode to be used to sort this column.
3190 If the sort_name is None it will use the attribute name for
3191 sorting. The sort type is the type of the sort column.
3192 The caption attribute is the text that appear as column caption
3193 (default is [('title_and_description', None, None, 'Episode'),])
3194 - title: (optional) The title of the window + heading
3195 - instructions: (optional) A one-line text describing what the
3196 user should select / what the selection is for
3197 - stock_ok_button: (optional) Will replace the "OK" button with
3198 another GTK+ stock item to be used for the
3199 affirmative button of the dialog (e.g. can
3200 be gtk.STOCK_DELETE when the episodes to be
3201 selected will be deleted after closing the
3202 dialog)
3203 - selection_buttons: (optional) A dictionary with labels as
3204 keys and callbacks as values; for each
3205 key a button will be generated, and when
3206 the button is clicked, the callback will
3207 be called for each episode and the return
3208 value of the callback (True or False) will
3209 be the new selected state of the episode
3210 - size_attribute: (optional) The name of an attribute of the
3211 supplied episode objects that can be used to
3212 calculate the size of an episode; set this to
3213 None if no total size calculation should be
3214 done (in cases where total size is useless)
3215 (default is 'length')
3216 - tooltip_attribute: (optional) The name of an attribute of
3217 the supplied episode objects that holds
3218 the text for the tooltips when hovering
3219 over an episode (default is 'description')
3222 finger_friendly_widgets = ['btnCancel', 'btnOK', 'btnCheckAll', 'btnCheckNone', 'treeviewEpisodes']
3224 COLUMN_INDEX = 0
3225 COLUMN_TOOLTIP = 1
3226 COLUMN_TOGGLE = 2
3227 COLUMN_ADDITIONAL = 3
3229 def new( self):
3230 if not hasattr( self, 'callback'):
3231 self.callback = None
3233 if not hasattr( self, 'episodes'):
3234 self.episodes = []
3236 if not hasattr( self, 'size_attribute'):
3237 self.size_attribute = 'length'
3239 if not hasattr(self, 'tooltip_attribute'):
3240 self.tooltip_attribute = 'description'
3242 if not hasattr( self, 'selection_buttons'):
3243 self.selection_buttons = {}
3245 if not hasattr( self, 'selected_default'):
3246 self.selected_default = False
3248 if not hasattr( self, 'selected'):
3249 self.selected = [self.selected_default]*len(self.episodes)
3251 if len(self.selected) < len(self.episodes):
3252 self.selected += [self.selected_default]*(len(self.episodes)-len(self.selected))
3254 if not hasattr( self, 'columns'):
3255 self.columns = (('title_and_description', None, None, _('Episode')),)
3257 if hasattr( self, 'title'):
3258 self.gPodderEpisodeSelector.set_title( self.title)
3259 self.labelHeading.set_markup( '<b><big>%s</big></b>' % saxutils.escape( self.title))
3261 if gpodder.interface == gpodder.MAEMO:
3262 self.labelHeading.hide()
3264 if hasattr( self, 'instructions'):
3265 self.labelInstructions.set_text( self.instructions)
3266 self.labelInstructions.show_all()
3268 if hasattr(self, 'stock_ok_button'):
3269 if self.stock_ok_button == 'gpodder-download':
3270 self.btnOK.set_image(gtk.image_new_from_stock(gtk.STOCK_GO_DOWN, gtk.ICON_SIZE_BUTTON))
3271 self.btnOK.set_label(_('Download'))
3272 else:
3273 self.btnOK.set_label(self.stock_ok_button)
3274 self.btnOK.set_use_stock(True)
3276 # check/uncheck column
3277 toggle_cell = gtk.CellRendererToggle()
3278 toggle_cell.connect( 'toggled', self.toggle_cell_handler)
3279 self.treeviewEpisodes.append_column( gtk.TreeViewColumn( '', toggle_cell, active=self.COLUMN_TOGGLE))
3281 next_column = self.COLUMN_ADDITIONAL
3282 for name, sort_name, sort_type, caption in self.columns:
3283 renderer = gtk.CellRendererText()
3284 renderer.set_property( 'ellipsize', pango.ELLIPSIZE_END)
3285 column = gtk.TreeViewColumn(caption, renderer, markup=next_column)
3286 column.set_resizable( True)
3287 # Only set "expand" on the first column (so more text is displayed there)
3288 column.set_expand(next_column == self.COLUMN_ADDITIONAL)
3289 if sort_name is not None:
3290 column.set_sort_column_id(next_column+1)
3291 else:
3292 column.set_sort_column_id(next_column)
3293 self.treeviewEpisodes.append_column( column)
3294 next_column += 1
3296 if sort_name is not None:
3297 # add the sort column
3298 column = gtk.TreeViewColumn()
3299 column.set_visible(False)
3300 self.treeviewEpisodes.append_column( column)
3301 next_column += 1
3303 column_types = [ gobject.TYPE_INT, gobject.TYPE_STRING, gobject.TYPE_BOOLEAN ]
3304 # add string column type plus sort column type if it exists
3305 for name, sort_name, sort_type, caption in self.columns:
3306 column_types.append(gobject.TYPE_STRING)
3307 if sort_name is not None:
3308 column_types.append(sort_type)
3309 self.model = gtk.ListStore( *column_types)
3311 tooltip = None
3312 for index, episode in enumerate( self.episodes):
3313 if self.tooltip_attribute is not None:
3314 try:
3315 tooltip = getattr(episode, self.tooltip_attribute)
3316 except:
3317 log('Episode object %s does not have tooltip attribute: "%s"', episode, self.tooltip_attribute, sender=self)
3318 tooltip = None
3319 row = [ index, tooltip, self.selected[index] ]
3320 for name, sort_name, sort_type, caption in self.columns:
3321 if not hasattr(episode, name):
3322 log('Warning: Missing attribute "%s"', name, sender=self)
3323 row.append(None)
3324 else:
3325 row.append(getattr( episode, name))
3327 if sort_name is not None:
3328 if not hasattr(episode, sort_name):
3329 log('Warning: Missing attribute "%s"', sort_name, sender=self)
3330 row.append(None)
3331 else:
3332 row.append(getattr( episode, sort_name))
3333 self.model.append( row)
3335 # connect to tooltip signals
3336 if self.tooltip_attribute is not None:
3337 try:
3338 self.treeviewEpisodes.set_property('has-tooltip', True)
3339 self.treeviewEpisodes.connect('query-tooltip', self.treeview_episodes_query_tooltip)
3340 except:
3341 log('I cannot set has-tooltip/query-tooltip (need at least PyGTK 2.12)', sender=self)
3342 self.last_tooltip_episode = None
3343 self.episode_list_can_tooltip = True
3345 self.treeviewEpisodes.connect('button-press-event', self.treeview_episodes_button_pressed)
3346 self.treeviewEpisodes.set_rules_hint( True)
3347 self.treeviewEpisodes.set_model( self.model)
3348 self.treeviewEpisodes.columns_autosize()
3349 self.calculate_total_size()
3351 def treeview_episodes_query_tooltip(self, treeview, x, y, keyboard_tooltip, tooltip):
3352 # With get_bin_window, we get the window that contains the rows without
3353 # the header. The Y coordinate of this window will be the height of the
3354 # treeview header. This is the amount we have to subtract from the
3355 # event's Y coordinate to get the coordinate to pass to get_path_at_pos
3356 (x_bin, y_bin) = treeview.get_bin_window().get_position()
3357 y -= x_bin
3358 y -= y_bin
3359 (path, column, rx, ry) = treeview.get_path_at_pos(x, y) or (None,)*4
3361 if not self.episode_list_can_tooltip:
3362 self.last_tooltip_episode = None
3363 return False
3365 if path is not None:
3366 model = treeview.get_model()
3367 iter = model.get_iter(path)
3368 index = model.get_value(iter, self.COLUMN_INDEX)
3369 description = model.get_value(iter, self.COLUMN_TOOLTIP)
3370 if self.last_tooltip_episode is not None and self.last_tooltip_episode != index:
3371 self.last_tooltip_episode = None
3372 return False
3373 self.last_tooltip_episode = index
3375 if description is not None:
3376 tooltip.set_text(description)
3377 return True
3378 else:
3379 return False
3381 self.last_tooltip_episode = None
3382 return False
3384 def treeview_episodes_button_pressed(self, treeview, event):
3385 if event.button == 3:
3386 menu = gtk.Menu()
3388 if len(self.selection_buttons):
3389 for label in self.selection_buttons:
3390 item = gtk.MenuItem(label)
3391 item.connect('activate', self.custom_selection_button_clicked, label)
3392 menu.append(item)
3393 menu.append(gtk.SeparatorMenuItem())
3395 item = gtk.MenuItem(_('Select all'))
3396 item.connect('activate', self.on_btnCheckAll_clicked)
3397 menu.append(item)
3399 item = gtk.MenuItem(_('Select none'))
3400 item.connect('activate', self.on_btnCheckNone_clicked)
3401 menu.append(item)
3403 menu.show_all()
3404 # Disable tooltips while we are showing the menu, so
3405 # the tooltip will not appear over the menu
3406 self.episode_list_can_tooltip = False
3407 menu.connect('deactivate', lambda menushell: self.episode_list_allow_tooltips())
3408 menu.popup(None, None, None, event.button, event.time)
3410 return True
3412 def episode_list_allow_tooltips(self):
3413 self.episode_list_can_tooltip = True
3415 def calculate_total_size( self):
3416 if self.size_attribute is not None:
3417 (total_size, count) = (0, 0)
3418 for episode in self.get_selected_episodes():
3419 try:
3420 total_size += int(getattr( episode, self.size_attribute))
3421 count += 1
3422 except:
3423 log( 'Cannot get size for %s', episode.title, sender = self)
3425 text = []
3426 if count == 0:
3427 text.append(_('Nothing selected'))
3428 elif count == 1:
3429 text.append(_('One episode selected'))
3430 else:
3431 text.append(_('%d episodes selected') % count)
3432 if total_size > 0:
3433 text.append(_('total size: %s') % gl.format_filesize(total_size))
3434 self.labelTotalSize.set_text(', '.join(text))
3435 self.btnOK.set_sensitive(count>0)
3436 else:
3437 self.btnOK.set_sensitive(False)
3438 for index, row in enumerate(self.model):
3439 if self.model.get_value(row.iter, self.COLUMN_TOGGLE) == True:
3440 self.btnOK.set_sensitive(True)
3441 break
3442 self.labelTotalSize.set_text('')
3444 def toggle_cell_handler( self, cell, path):
3445 model = self.treeviewEpisodes.get_model()
3446 model[path][self.COLUMN_TOGGLE] = not model[path][self.COLUMN_TOGGLE]
3448 self.calculate_total_size()
3450 def custom_selection_button_clicked(self, button, label):
3451 callback = self.selection_buttons[label]
3453 for index, row in enumerate( self.model):
3454 new_value = callback( self.episodes[index])
3455 self.model.set_value( row.iter, self.COLUMN_TOGGLE, new_value)
3457 self.calculate_total_size()
3459 def on_btnCheckAll_clicked( self, widget):
3460 for row in self.model:
3461 self.model.set_value( row.iter, self.COLUMN_TOGGLE, True)
3463 self.calculate_total_size()
3465 def on_btnCheckNone_clicked( self, widget):
3466 for row in self.model:
3467 self.model.set_value( row.iter, self.COLUMN_TOGGLE, False)
3469 self.calculate_total_size()
3471 def get_selected_episodes( self):
3472 selected_episodes = []
3474 for index, row in enumerate( self.model):
3475 if self.model.get_value( row.iter, self.COLUMN_TOGGLE) == True:
3476 selected_episodes.append( self.episodes[self.model.get_value( row.iter, self.COLUMN_INDEX)])
3478 return selected_episodes
3480 def on_btnOK_clicked( self, widget):
3481 self.gPodderEpisodeSelector.destroy()
3482 if self.callback is not None:
3483 self.callback( self.get_selected_episodes())
3485 def on_btnCancel_clicked( self, widget):
3486 self.gPodderEpisodeSelector.destroy()
3487 if self.callback is not None:
3488 self.callback([])
3490 class gPodderConfigEditor(GladeWidget):
3491 finger_friendly_widgets = ['btnShowAll', 'btnClose', 'configeditor']
3493 def new(self):
3494 name_column = gtk.TreeViewColumn(_('Setting'))
3495 name_renderer = gtk.CellRendererText()
3496 name_column.pack_start(name_renderer)
3497 name_column.add_attribute(name_renderer, 'text', 0)
3498 name_column.add_attribute(name_renderer, 'style', 5)
3499 self.configeditor.append_column(name_column)
3501 value_column = gtk.TreeViewColumn(_('Set to'))
3502 value_check_renderer = gtk.CellRendererToggle()
3503 value_column.pack_start(value_check_renderer, expand=False)
3504 value_column.add_attribute(value_check_renderer, 'active', 7)
3505 value_column.add_attribute(value_check_renderer, 'visible', 6)
3506 value_column.add_attribute(value_check_renderer, 'activatable', 6)
3507 value_check_renderer.connect('toggled', self.value_toggled)
3509 value_renderer = gtk.CellRendererText()
3510 value_column.pack_start(value_renderer)
3511 value_column.add_attribute(value_renderer, 'text', 2)
3512 value_column.add_attribute(value_renderer, 'visible', 4)
3513 value_column.add_attribute(value_renderer, 'editable', 4)
3514 value_column.add_attribute(value_renderer, 'style', 5)
3515 value_renderer.connect('edited', self.value_edited)
3516 self.configeditor.append_column(value_column)
3518 self.model = gl.config.model()
3519 self.filter = self.model.filter_new()
3520 self.filter.set_visible_func(self.visible_func)
3522 self.configeditor.set_model(self.filter)
3523 self.configeditor.set_rules_hint(True)
3525 def visible_func(self, model, iter, user_data=None):
3526 text = self.entryFilter.get_text().lower()
3527 if text == '':
3528 return True
3529 else:
3530 # either the variable name or its value
3531 return (text in model.get_value(iter, 0).lower() or
3532 text in model.get_value(iter, 2).lower())
3534 def value_edited(self, renderer, path, new_text):
3535 model = self.configeditor.get_model()
3536 iter = model.get_iter(path)
3537 name = model.get_value(iter, 0)
3538 type_cute = model.get_value(iter, 1)
3540 if not gl.config.update_field(name, new_text):
3541 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))
3543 def value_toggled(self, renderer, path):
3544 model = self.configeditor.get_model()
3545 iter = model.get_iter(path)
3546 field_name = model.get_value(iter, 0)
3547 field_type = model.get_value(iter, 3)
3549 # Flip the boolean config flag
3550 if field_type == bool:
3551 gl.config.toggle_flag(field_name)
3553 def on_entryFilter_changed(self, widget):
3554 self.filter.refilter()
3556 def on_btnShowAll_clicked(self, widget):
3557 self.entryFilter.set_text('')
3558 self.entryFilter.grab_focus()
3560 def on_btnClose_clicked(self, widget):
3561 self.gPodderConfigEditor.destroy()
3564 def main():
3565 gobject.threads_init()
3566 gtk.window_set_default_icon_name( 'gpodder')
3568 gPodder().run()