A bulk of Fremantle (Maemo 5)-related changes
[gpodder.git] / src / gpodder / gtkui / frmntl / episodeselector.py
blob41034f65772061cacb7eecdc56349d6beaa7dbc7
1 # -*- coding: utf-8 -*-
3 # gPodder - A media aggregator and podcast client
4 # Copyright (c) 2005-2009 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 gtk
21 import hildon
22 import pango
23 from xml.sax import saxutils
25 import gpodder
26 _ = gpodder.gettext
28 from gpodder import util
29 from gpodder.liblogger import log
31 from gpodder.gtkui.interface.common import BuilderWidget
33 class gPodderEpisodeSelector(BuilderWidget):
34 """Episode selection dialog
36 Optional keyword arguments that modify the behaviour of this dialog:
38 - callback: Function that takes 1 parameter which is a list of
39 the selected episodes (or empty list when none selected)
40 - remove_callback: Function that takes 1 parameter which is a list
41 of episodes that should be "removed" (see below)
42 (default is None, which means remove not possible)
43 - remove_action: Label for the "remove" action (default is "Remove")
44 - remove_finished: Callback after all remove callbacks have finished
45 (default is None, also depends on remove_callback)
46 It will get a list of episode URLs that have been
47 removed, so the main UI can update those
48 - episodes: List of episodes that are presented for selection
49 - selected: (optional) List of boolean variables that define the
50 default checked state for the given episodes
51 - selected_default: (optional) The default boolean value for the
52 checked state if no other value is set
53 (default is False)
54 - columns: List of (name, sort_name, sort_type, caption) pairs for the
55 columns, the name is the attribute name of the episode to be
56 read from each episode object. The sort name is the
57 attribute name of the episode to be used to sort this column.
58 If the sort_name is None it will use the attribute name for
59 sorting. The sort type is the type of the sort column.
60 The caption attribute is the text that appear as column caption
61 (default is [('title_markup', None, None, 'Episode'),])
62 - title: (optional) The title of the window + heading
63 - instructions: (optional) A one-line text describing what the
64 user should select / what the selection is for
65 - stock_ok_button: (optional) Will replace the "OK" button with
66 another GTK+ stock item to be used for the
67 affirmative button of the dialog (e.g. can
68 be gtk.STOCK_DELETE when the episodes to be
69 selected will be deleted after closing the
70 dialog)
71 - selection_buttons: (optional) A dictionary with labels as
72 keys and callbacks as values; for each
73 key a button will be generated, and when
74 the button is clicked, the callback will
75 be called for each episode and the return
76 value of the callback (True or False) will
77 be the new selected state of the episode
78 - size_attribute: (optional) The name of an attribute of the
79 supplied episode objects that can be used to
80 calculate the size of an episode; set this to
81 None if no total size calculation should be
82 done (in cases where total size is useless)
83 (default is 'length')
84 - tooltip_attribute: (optional) The name of an attribute of
85 the supplied episode objects that holds
86 the text for the tooltips when hovering
87 over an episode (default is 'description')
89 """
90 finger_friendly_widgets = ['btnRemoveAction', 'btnOK']
92 COLUMN_INDEX = 0
93 COLUMN_TOOLTIP = 1
94 COLUMN_TOGGLE = 2
95 COLUMN_ADDITIONAL = 3
97 def new( self):
98 self._config.connect_gtk_window(self.gPodderEpisodeSelector, 'episode_selector', True)
99 if not hasattr( self, 'callback'):
100 self.callback = None
102 if not hasattr(self, 'remove_callback'):
103 self.remove_callback = None
105 if not hasattr(self, 'remove_action'):
106 self.remove_action = _('Remove')
108 if not hasattr(self, 'remove_finished'):
109 self.remove_finished = None
111 if not hasattr( self, 'episodes'):
112 self.episodes = []
114 if not hasattr( self, 'size_attribute'):
115 self.size_attribute = 'length'
117 if not hasattr(self, 'tooltip_attribute'):
118 self.tooltip_attribute = 'description'
120 if not hasattr( self, 'selection_buttons'):
121 self.selection_buttons = {}
123 if not hasattr( self, 'selected_default'):
124 self.selected_default = False
126 if not hasattr( self, 'selected'):
127 self.selected = [self.selected_default]*len(self.episodes)
129 if len(self.selected) < len(self.episodes):
130 self.selected += [self.selected_default]*(len(self.episodes)-len(self.selected))
132 if not hasattr( self, 'columns'):
133 self.columns = (('title_markup', None, None, _('Episode')),)
135 if hasattr( self, 'title'):
136 self.gPodderEpisodeSelector.set_title( self.title)
138 if hasattr(self, 'instructions'):
139 self.show_message(self.instructions)
141 if self.remove_callback is not None:
142 self.btnRemoveAction.show()
143 self.btnRemoveAction.set_label(self.remove_action)
145 if hasattr(self, 'stock_ok_button'):
146 if self.stock_ok_button == 'gpodder-download':
147 self.btnOK.set_image(gtk.image_new_from_stock(gtk.STOCK_GO_DOWN, gtk.ICON_SIZE_BUTTON))
148 self.btnOK.set_label(_('Download'))
149 else:
150 self.btnOK.set_label(self.stock_ok_button)
151 self.btnOK.set_use_stock(True)
153 # Work around Maemo bug #4718
154 self.btnOK.set_name('HildonButton-finger')
155 self.btnRemoveAction.set_name('HildonButton-finger')
157 # Make sure the window comes up quick
158 self.main_window.show()
159 self.main_window.present()
160 while gtk.events_pending():
161 gtk.main_iteration(False)
163 next_column = self.COLUMN_ADDITIONAL
164 for name, sort_name, sort_type, caption in self.columns:
165 renderer = gtk.CellRendererText()
166 if next_column < self.COLUMN_ADDITIONAL + 2:
167 renderer.set_property('ellipsize', pango.ELLIPSIZE_END)
168 column = gtk.TreeViewColumn(caption, renderer, markup=next_column)
169 column.set_resizable( True)
170 # Only set "expand" on the first column
171 if next_column < self.COLUMN_ADDITIONAL + 1:
172 column.set_expand(True)
173 if sort_name is not None:
174 column.set_sort_column_id(next_column+1)
175 else:
176 column.set_sort_column_id(next_column)
177 self.treeviewEpisodes.append_column( column)
178 next_column += 1
180 if sort_name is not None:
181 # add the sort column
182 column = gtk.TreeViewColumn()
183 column.set_visible(False)
184 self.treeviewEpisodes.append_column( column)
185 next_column += 1
187 column_types = [ int, str, bool ]
188 # add string column type plus sort column type if it exists
189 for name, sort_name, sort_type, caption in self.columns:
190 column_types.append(str)
191 if sort_name is not None:
192 column_types.append(sort_type)
193 self.model = gtk.ListStore( *column_types)
195 tooltip = None
196 for index, episode in enumerate( self.episodes):
197 if self.tooltip_attribute is not None:
198 try:
199 tooltip = getattr(episode, self.tooltip_attribute)
200 except:
201 log('Episode object %s does not have tooltip attribute: "%s"', episode, self.tooltip_attribute, sender=self)
202 tooltip = None
203 row = [ index, tooltip, self.selected[index] ]
204 for name, sort_name, sort_type, caption in self.columns:
205 if not hasattr(episode, name):
206 log('Warning: Missing attribute "%s"', name, sender=self)
207 row.append(None)
208 else:
209 row.append(getattr( episode, name))
211 if sort_name is not None:
212 if not hasattr(episode, sort_name):
213 log('Warning: Missing attribute "%s"', sort_name, sender=self)
214 row.append(None)
215 else:
216 row.append(getattr( episode, sort_name))
217 self.model.append( row)
219 self.treeviewEpisodes.set_rules_hint( True)
220 self.treeviewEpisodes.set_model( self.model)
221 self.treeviewEpisodes.columns_autosize()
222 self.calculate_total_size()
224 selection = self.treeviewEpisodes.get_selection()
225 selection.connect('changed', self.on_selection_changed)
226 selection.set_mode(gtk.SELECTION_MULTIPLE)
227 selection.unselect_all()
229 appmenu = hildon.AppMenu()
230 for action in (self.action_select_all, \
231 self.action_select_none):
232 button = gtk.Button()
233 action.connect_proxy(button)
234 appmenu.append(button)
236 if self.selection_buttons:
237 for label in self.selection_buttons:
238 button = gtk.Button(label)
239 button.connect('clicked', self.custom_selection_button_clicked, label)
240 appmenu.append(button)
242 appmenu.show_all()
243 self.main_window.set_app_menu(appmenu)
245 def on_selection_changed(self, selection):
246 self.calculate_total_size()
248 def on_treeview_button_release(self, widget, event):
249 selection = widget.get_selection()
250 self.on_selection_changed(widget.get_selection())
252 def on_select_all_button_clicked(self, widget):
253 selection = self.treeviewEpisodes.get_selection()
254 selection.select_all()
255 self.calculate_total_size()
257 def on_select_none_button_clicked(self, widget):
258 selection = self.treeviewEpisodes.get_selection()
259 selection.unselect_all()
260 self.calculate_total_size()
262 def on_close_button_clicked(self, widget):
263 self.on_btnCancel_clicked(widget)
265 def calculate_total_size( self):
266 if self.size_attribute is not None:
267 (total_size, count) = (0, 0)
268 for episode in self.get_selected_episodes():
269 try:
270 total_size += int(getattr( episode, self.size_attribute))
271 count += 1
272 except:
273 log( 'Cannot get size for %s', episode.title, sender = self)
275 text = []
276 if count == 0:
277 text.append(_('Nothing selected'))
278 elif count == 1:
279 text.append(_('One episode'))
280 else:
281 text.append(_('%d episodes') % count)
282 if total_size > 0:
283 text.append(_('size: %s') % util.format_filesize(total_size))
284 self.labelTotalSize.set_text(', '.join(text))
285 self.btnOK.set_sensitive(count>0)
286 self.btnRemoveAction.set_sensitive(count>0)
287 else:
288 selection = self.treeviewEpisodes.get_selection()
289 selected_rows = selection.count_selected_rows()
290 self.btnOK.set_sensitive(selected_rows > 0)
291 self.btnRemoveAction.set_sensitive(selected_rows > 0)
292 self.labelTotalSize.set_text('')
294 def custom_selection_button_clicked(self, button, label):
295 callback = self.selection_buttons[label]
297 selection = self.treeviewEpisodes.get_selection()
298 selection.unselect_all()
299 for index, row in enumerate(self.model):
300 if callback(self.episodes[index]):
301 selection.select_path(row.path)
303 self.calculate_total_size()
305 def on_remove_action_activate(self, widget):
306 episodes = self.get_selected_episodes(remove_episodes=True)
308 urls = []
309 for episode in episodes:
310 urls.append(episode.url)
311 self.remove_callback(episode)
313 if self.remove_finished is not None:
314 self.remove_finished(urls)
315 self.calculate_total_size()
317 def get_selected_episodes( self, remove_episodes=False):
318 selection = self.treeviewEpisodes.get_selection()
319 model, paths = selection.get_selected_rows()
321 selected_episodes = [self.episodes[model.get_value(\
322 model.get_iter(path), self.COLUMN_INDEX)] \
323 for path in paths]
325 if remove_episodes:
326 for episode in selected_episodes:
327 index = self.episodes.index(episode)
328 iter = self.model.get_iter_first()
329 while iter is not None:
330 if self.model.get_value(iter, self.COLUMN_INDEX) == index:
331 self.model.remove(iter)
332 break
333 iter = self.model.iter_next(iter)
335 return selected_episodes
337 def on_btnOK_clicked( self, widget):
338 selected = self.get_selected_episodes()
339 self.gPodderEpisodeSelector.destroy()
340 if self.callback is not None:
341 self.callback(selected)
343 def on_btnCancel_clicked(self, widget):
344 self.gPodderEpisodeSelector.destroy()
345 if self.callback is not None:
346 self.callback([])