Use dict-based format strings for numbers (bug 1165)
[gpodder.git] / src / gpodder / gtkui / maemo / episodeselector.py
blob9831968797ebdcc22d0f87feeb4bfa82c5ce4129
1 # -*- coding: utf-8 -*-
3 # gPodder - A media aggregator and podcast client
4 # Copyright (c) 2005-2010 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 pango
23 import gpodder
25 _ = gpodder.gettext
26 N_ = gpodder.ngettext
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', 'btnCancel', '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.labelInstructions.set_text( self.instructions)
140 self.labelInstructions.show_all()
142 if self.remove_callback is not None:
143 self.btnRemoveAction.show()
144 self.btnRemoveAction.set_label(self.remove_action)
146 if hasattr(self, 'stock_ok_button'):
147 if self.stock_ok_button == 'gpodder-download':
148 self.btnOK.set_image(gtk.image_new_from_stock(gtk.STOCK_GO_DOWN, gtk.ICON_SIZE_BUTTON))
149 self.btnOK.set_label(_('Download'))
150 else:
151 self.btnOK.set_label(self.stock_ok_button)
152 self.btnOK.set_use_stock(True)
154 # Make sure the window comes up quick
155 self.main_window.show()
156 self.main_window.present()
157 while gtk.events_pending():
158 gtk.main_iteration(False)
160 if getattr(self, 'show_notification', False) and hasattr(self, 'title'):
161 self.show_message(self.title)
163 toggle_cell = gtk.CellRendererToggle()
164 toggle_cell.set_fixed_size(50, 50)
165 self.treeviewEpisodes.append_column( gtk.TreeViewColumn( '', toggle_cell, active=self.COLUMN_TOGGLE))
167 next_column = self.COLUMN_ADDITIONAL
168 for name, sort_name, sort_type, caption in self.columns:
169 renderer = gtk.CellRendererText()
170 if next_column < self.COLUMN_ADDITIONAL + 2:
171 renderer.set_property('ellipsize', pango.ELLIPSIZE_END)
172 column = gtk.TreeViewColumn(caption, renderer, markup=next_column)
173 column.set_resizable( True)
174 # Only set "expand" on the first column
175 if next_column < self.COLUMN_ADDITIONAL + 1:
176 column.set_expand(True)
177 if sort_name is not None:
178 column.set_sort_column_id(next_column+1)
179 else:
180 column.set_sort_column_id(next_column)
181 self.treeviewEpisodes.append_column( column)
182 next_column += 1
184 if sort_name is not None:
185 # add the sort column
186 column = gtk.TreeViewColumn()
187 column.set_visible(False)
188 self.treeviewEpisodes.append_column( column)
189 next_column += 1
191 column_types = [ int, str, bool ]
192 # add string column type plus sort column type if it exists
193 for name, sort_name, sort_type, caption in self.columns:
194 column_types.append(str)
195 if sort_name is not None:
196 column_types.append(sort_type)
197 self.model = gtk.ListStore( *column_types)
199 tooltip = None
200 for index, episode in enumerate( self.episodes):
201 if self.tooltip_attribute is not None:
202 try:
203 tooltip = getattr(episode, self.tooltip_attribute)
204 except:
205 log('Episode object %s does not have tooltip attribute: "%s"', episode, self.tooltip_attribute, sender=self)
206 tooltip = None
207 row = [ index, tooltip, self.selected[index] ]
208 for name, sort_name, sort_type, caption in self.columns:
209 if not hasattr(episode, name):
210 log('Warning: Missing attribute "%s"', name, sender=self)
211 row.append(None)
212 else:
213 row.append(getattr( episode, name))
215 if sort_name is not None:
216 if not hasattr(episode, sort_name):
217 log('Warning: Missing attribute "%s"', sort_name, sender=self)
218 row.append(None)
219 else:
220 row.append(getattr( episode, sort_name))
221 self.model.append( row)
223 self.treeviewEpisodes.set_rules_hint( True)
224 self.treeviewEpisodes.set_model( self.model)
225 self.treeviewEpisodes.columns_autosize()
226 self.calculate_total_size()
228 menu = gtk.Menu()
229 menu.append(self.action_select_all.create_menu_item())
230 menu.append(self.action_select_none.create_menu_item())
231 menu.append(gtk.SeparatorMenuItem())
232 menu.append(self.action_invert_selection.create_menu_item())
233 menu.append(gtk.SeparatorMenuItem())
235 self.treeviewEpisodes.connect('button-release-event', \
236 self.on_treeview_button_release)
238 if self.selection_buttons:
239 for label in self.selection_buttons:
240 item = gtk.MenuItem(label)
241 item.connect('activate', self.custom_selection_button_clicked, label)
242 menu.append(item)
243 menu.append(gtk.SeparatorMenuItem())
245 menu.append(self.action_close.create_menu_item())
246 self.main_window.set_menu(self.set_finger_friendly(menu))
247 self.main_window.connect('key-press-event', self._on_key_press_event)
249 def _on_key_press_event(self, widget, event):
250 if event.keyval == gtk.keysyms.Escape:
251 self.on_close_button_clicked(widget)
252 return True
253 else:
254 return False
256 def on_treeview_button_release(self, widget, event):
257 selection = widget.get_selection()
258 model, iter = selection.get_selected()
259 if iter is not None:
260 model.set_value(iter, self.COLUMN_TOGGLE, \
261 not model.get_value(iter, self.COLUMN_TOGGLE))
262 self.calculate_total_size()
264 def on_select_all_button_clicked(self, widget):
265 for row in self.model:
266 row[self.COLUMN_TOGGLE] = True
267 self.calculate_total_size()
269 def on_select_none_button_clicked(self, widget):
270 for row in self.model:
271 row[self.COLUMN_TOGGLE] = False
272 self.calculate_total_size()
274 def on_invert_selection_button_clicked(self, widget):
275 for row in self.model:
276 row[self.COLUMN_TOGGLE] = not row[self.COLUMN_TOGGLE]
277 self.calculate_total_size()
279 def on_close_button_clicked(self, widget):
280 self.on_btnCancel_clicked(widget)
282 def calculate_total_size( self):
283 if self.size_attribute is not None:
284 (total_size, count) = (0, 0)
285 for episode in self.get_selected_episodes():
286 try:
287 total_size += int(getattr( episode, self.size_attribute))
288 count += 1
289 except:
290 log( 'Cannot get size for %s', episode.title, sender = self)
292 text = []
293 if count == 0:
294 text.append(_('Nothing selected'))
295 else:
296 text.append(N_('%(count)d episode', '%(count)d episodes', count) % {'count':count})
298 if total_size > 0:
299 text.append(_('size: %s') % util.format_filesize(total_size))
300 self.labelTotalSize.set_text(', '.join(text))
301 self.btnOK.set_sensitive(count>0)
302 self.btnRemoveAction.set_sensitive(count>0)
303 if count > 0:
304 self.btnCancel.set_label(gtk.STOCK_CANCEL)
305 else:
306 self.btnCancel.set_label(gtk.STOCK_CLOSE)
307 else:
308 self.btnOK.set_sensitive(False)
309 self.btnRemoveAction.set_sensitive(False)
310 for index, row in enumerate(self.model):
311 if self.model.get_value(row.iter, self.COLUMN_TOGGLE) == True:
312 self.btnOK.set_sensitive(True)
313 self.btnRemoveAction.set_sensitive(True)
314 break
315 self.labelTotalSize.set_text('')
317 def custom_selection_button_clicked(self, button, label):
318 callback = self.selection_buttons[label]
320 for index, row in enumerate( self.model):
321 new_value = callback( self.episodes[index])
322 self.model.set_value( row.iter, self.COLUMN_TOGGLE, new_value)
324 self.calculate_total_size()
326 def on_remove_action_activate(self, widget):
327 episodes = self.get_selected_episodes(remove_episodes=True)
329 urls = []
330 for episode in episodes:
331 urls.append(episode.url)
332 self.remove_callback(episode)
334 if self.remove_finished is not None:
335 self.remove_finished(urls)
336 self.calculate_total_size()
338 # Close the window when there are no episodes left
339 model = self.treeviewEpisodes.get_model()
340 if model.get_iter_first() is None:
341 self.on_btnCancel_clicked(None)
343 def get_selected_episodes( self, remove_episodes=False):
344 selected_episodes = []
346 for index, row in enumerate( self.model):
347 if self.model.get_value( row.iter, self.COLUMN_TOGGLE) == True:
348 selected_episodes.append( self.episodes[self.model.get_value( row.iter, self.COLUMN_INDEX)])
350 if remove_episodes:
351 for episode in selected_episodes:
352 index = self.episodes.index(episode)
353 iter = self.model.get_iter_first()
354 while iter is not None:
355 if self.model.get_value(iter, self.COLUMN_INDEX) == index:
356 self.model.remove(iter)
357 break
358 iter = self.model.iter_next(iter)
360 return selected_episodes
362 def on_btnOK_clicked( self, widget):
363 selected = self.get_selected_episodes()
364 self.gPodderEpisodeSelector.destroy()
365 if self.callback is not None:
366 self.callback(selected)
368 def on_btnCancel_clicked(self, widget):
369 self.gPodderEpisodeSelector.destroy()
370 if self.callback is not None:
371 self.callback([])