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/>.
22 # gpodder.gtkui.frmntl.model -- Model customizations for Maemo 5 (2009-11-16)
33 from gpodder
.gtkui
import download
34 from gpodder
.gtkui
import model
35 from gpodder
.gtkui
.frmntl
import style
37 from gpodder
import util
39 class DownloadStatusModel(download
.DownloadStatusModel
):
41 download
.DownloadStatusModel
.__init
__(self
)
42 head_font
= style
.get_font_desc('SystemFont')
43 head_color
= style
.get_color('ButtonTextColor')
44 head
= (head_font
.to_string(), head_color
.to_string())
45 head
= '<span font_desc="%s" foreground="%s">%%s</span>' % head
46 sub_font
= style
.get_font_desc('SmallSystemFont')
47 sub_color
= style
.get_color('SecondaryTextColor')
48 sub
= (sub_font
.to_string(), sub_color
.to_string())
49 sub
= '<span font_desc="%s" foreground="%s">%%s - %%s</span>' % sub
50 self
._markup
_template
= '\n'.join((head
, sub
))
52 def _format_message(self
, episode
, message
, podcast
):
53 return self
._markup
_template
% (episode
, message
, podcast
)
56 class EpisodeListModel(gtk
.GenericTreeModel
):
59 C_URL
, C_TITLE
, C_FILESIZE_TEXT
, C_EPISODE
, C_STATUS_ICON
, \
60 C_PUBLISHED_TEXT
, C_DESCRIPTION
, C_TOOLTIP
, \
61 C_VIEW_SHOW_UNDELETED
, C_VIEW_SHOW_DOWNLOADED
, \
62 C_VIEW_SHOW_UNPLAYED
, C_FILESIZE
, C_PUBLISHED
, \
63 C_TIME
, C_TIME1_VISIBLE
, C_TIME2_VISIBLE
, \
64 C_LOCKED
= range(N_COLUMNS
)
66 DATA_TYPES
= (str, str, str, object, str , str, str, \
67 str, bool, bool, bool, int, int, str, bool, bool, \
70 SEARCH_COLUMNS
= (C_TITLE
,)
72 VIEW_ALL
, VIEW_UNDELETED
, VIEW_DOWNLOADED
, VIEW_UNPLAYED
= range(4)
74 # ---------------------
76 def on_get_flags(self
):
77 return gtk
.TREE_MODEL_LIST_ONLY
79 def on_get_n_columns(self
):
82 def on_get_column_type(self
, index
):
83 return self
.DATA_TYPES
[index
]
85 def on_get_iter(self
, path
):
88 def on_get_path(self
, rowref
):
91 def on_get_value(self
, rowref
, column
):
92 if rowref
>= len(self
._episodes
):
95 episode
= self
._episodes
[rowref
]
96 downloading
= self
._downloading
98 if column
== self
.C_URL
:
100 elif column
== self
.C_TITLE
:
102 elif column
== self
.C_FILESIZE_TEXT
:
103 return self
._format
_filesize
(episode
)
104 elif column
== self
.C_EPISODE
:
106 elif column
== self
.C_STATUS_ICON
:
107 if downloading(episode
):
108 return self
.ICON_DOWNLOADING
109 elif episode
.state
== gpodder
.STATE_DELETED
:
110 return self
.ICON_DELETED
111 elif episode
.state
== gpodder
.STATE_NORMAL
and \
112 not episode
.is_played
and \
113 not downloading(episode
):
115 elif episode
.state
== gpodder
.STATE_DOWNLOADED
:
116 filename
= episode
.local_filename(create
=False, \
119 file_type
= episode
.file_type()
120 if file_type
== 'audio':
121 status_icon
= self
.ICON_AUDIO_FILE
122 elif file_type
== 'video':
123 status_icon
= self
.ICON_VIDEO_FILE
124 elif file_type
== 'image':
125 status_icon
= self
.ICON_IMAGE_FILE
127 status_icon
= self
.ICON_GENERIC_FILE
132 icon_theme
= gtk
.icon_theme_get_default()
133 if filename
is not None and have_gio
:
134 file = gio
.File(filename
)
135 if file.query_exists():
136 file_info
= file.query_info('*')
137 icon
= file_info
.get_icon()
138 for icon_name
in icon
.get_names():
139 if icon_theme
.has_icon(icon_name
):
145 elif column
== self
.C_PUBLISHED_TEXT
:
146 return episode
.cute_pubdate()
147 elif column
== self
.C_DESCRIPTION
:
148 return self
._format
_description
(episode
)
149 elif column
== self
.C_TOOLTIP
:
151 elif column
== self
.C_VIEW_SHOW_UNDELETED
:
152 return episode
.state
!= gpodder
.STATE_DELETED
or downloading(episode
)
153 elif column
== self
.C_VIEW_SHOW_DOWNLOADED
:
154 return episode
.state
== gpodder
.STATE_DOWNLOADED
or \
155 (episode
.state
== gpodder
.STATE_NORMAL
and \
156 not episode
.is_played
) or \
158 elif column
== self
.C_VIEW_SHOW_UNPLAYED
:
159 return (not episode
.is_played
and (episode
.state
in \
160 (gpodder
.STATE_DOWNLOADED
, gpodder
.STATE_NORMAL
))) or \
162 elif column
== self
.C_FILESIZE
:
163 return episode
.length
164 elif column
== self
.C_PUBLISHED
:
165 return episode
.pubDate
166 elif column
== self
.C_TIME
:
167 return episode
.get_play_info_string()
168 elif column
== self
.C_TIME1_VISIBLE
:
169 return (episode
.total_time
and not episode
.current_position
)
170 elif column
== self
.C_TIME2_VISIBLE
:
171 return (episode
.total_time
and episode
.current_position
)
172 elif column
== self
.C_LOCKED
:
173 return episode
.is_locked
and \
174 episode
.state
== gpodder
.STATE_DOWNLOADED
and \
175 episode
.file_exists()
177 raise Exception('could not find column index: ' + str(column
))
179 def on_iter_next(self
, rowref
):
180 if len(self
._episodes
) > rowref
+ 1:
185 def on_iter_children(self
, parent
):
192 def on_iter_has_child(self
, rowref
):
195 def on_iter_n_children(self
, rowref
):
197 return len(self
._episodes
)
201 def on_iter_nth_child(self
, parent
, n
):
207 def on_iter_parent(self
, child
):
210 # ---------------------
214 gtk
.GenericTreeModel
.__init
__(self
)
216 self
._downloading
= None
217 self
._include
_description
= False
218 self
._generate
_thumbnails
= False
222 # Filter to allow hiding some episodes
223 self
._filter
= self
.filter_new()
224 self
._sorter
= gtk
.TreeModelSort(self
._filter
)
225 self
._view
_mode
= self
.VIEW_ALL
226 self
._search
_term
= None
227 self
._filter
.set_visible_func(self
._filter
_visible
_func
)
229 # Are we currently showing the "all episodes" view?
230 self
._all
_episodes
_view
= False
232 # "ICON" is used to mark icon names in source files
235 self
._icon
_cache
= {}
236 self
.ICON_AUDIO_FILE
= ICON('general_audio_file')
237 self
.ICON_VIDEO_FILE
= ICON('general_video_file')
238 self
.ICON_IMAGE_FILE
= ICON('general_image')
239 self
.ICON_GENERIC_FILE
= ICON('filemanager_unknown_file')
240 self
.ICON_DOWNLOADING
= ICON('email_inbox')
241 self
.ICON_DELETED
= ICON('camera_delete')
242 self
.ICON_UNPLAYED
= ICON('emblem-new')
243 self
.ICON_LOCKED
= ICON('emblem-readonly')
244 self
.ICON_MISSING
= ICON('emblem-unreadable')
245 self
.ICON_NEW
= gtk
.STOCK_ABOUT
247 normal_font
= style
.get_font_desc('SystemFont')
248 normal_color
= style
.get_color('DefaultTextColor')
249 normal
= (normal_font
.to_string(), normal_color
.to_string())
250 self
._normal
_markup
= '<span font_desc="%s" foreground="%s">%%s</span>' % normal
252 active_font
= style
.get_font_desc('SystemFont')
253 active_color
= style
.get_color('ActiveTextColor')
254 active
= (active_font
.to_string(), active_color
.to_string())
255 self
._active
_markup
= '<span font_desc="%s" foreground="%s">%%s</span>' % active
257 sub_font
= style
.get_font_desc('SmallSystemFont')
258 sub_color
= style
.get_color('SecondaryTextColor')
259 sub
= (sub_font
.to_string(), sub_color
.to_string())
260 sub
= '\n<span font_desc="%s" foreground="%s">%%s</span>' % sub
262 self
._unplayed
_markup
= self
._normal
_markup
+ sub
263 self
._active
_markup
+= sub
267 def _format_filesize(self
, episode
):
268 if episode
.length
> 0:
269 return util
.format_filesize(episode
.length
, 1)
274 def _filter_visible_func(self
, model
, iter):
275 # If searching is active, set visibility based on search text
276 if self
._search
_term
is not None:
277 key
= self
._search
_term
.lower()
278 return any((key
in (model
.get_value(iter, column
) or '').lower()) for column
in self
.SEARCH_COLUMNS
)
280 if self
._view
_mode
== self
.VIEW_ALL
:
282 elif self
._view
_mode
== self
.VIEW_UNDELETED
:
283 return model
.get_value(iter, self
.C_VIEW_SHOW_UNDELETED
)
284 elif self
._view
_mode
== self
.VIEW_DOWNLOADED
:
285 return model
.get_value(iter, self
.C_VIEW_SHOW_DOWNLOADED
)
286 elif self
._view
_mode
== self
.VIEW_UNPLAYED
:
287 return model
.get_value(iter, self
.C_VIEW_SHOW_UNPLAYED
)
291 def get_filtered_model(self
):
292 """Returns a filtered version of this episode model
294 The filtered version should be displayed in the UI,
295 as this model can have some filters set that should
296 be reflected in the UI.
300 def set_view_mode(self
, new_mode
):
301 """Sets a new view mode for this model
303 After setting the view mode, the filtered model
304 might be updated to reflect the new mode."""
305 if self
._view
_mode
!= new_mode
:
306 self
._view
_mode
= new_mode
307 self
._filter
.refilter()
309 def get_view_mode(self
):
310 """Returns the currently-set view mode"""
311 return self
._view
_mode
313 def set_search_term(self
, new_term
):
314 if self
._search
_term
!= new_term
:
315 self
._search
_term
= new_term
316 self
._filter
.refilter()
318 def get_search_term(self
):
319 return self
._search
_term
322 def _format_description(self
, episode
):
323 if self
._downloading
(episode
):
324 sub
= _('in downloads list')
325 if self
._all
_episodes
_view
:
326 sub
= '; '.join((sub
, _('from %s') % cgi
.escape(episode
.channel
.title
,)))
327 return self
._unplayed
_markup
% (cgi
.escape(episode
.title
), sub
)
328 elif episode
.is_played
:
329 if self
._all
_episodes
_view
:
330 sub
= _('from %s') % cgi
.escape(episode
.channel
.title
,)
331 return self
._unplayed
_markup
% (cgi
.escape(episode
.title
), sub
)
333 return self
._normal
_markup
% (cgi
.escape(episode
.title
),)
335 if episode
.was_downloaded(and_exists
=True):
336 sub
= _('unplayed download')
338 sub
= _('new episode')
340 if self
._all
_episodes
_view
:
341 sub
= '; '.join((sub
, _('from %s') % cgi
.escape(episode
.channel
.title
,)))
343 return self
._active
_markup
% (cgi
.escape(episode
.title
), sub
)
346 count
= len(self
._episodes
)
347 for i
in reversed(range(count
)):
348 self
.emit('row-deleted', (i
,))
351 def replace_from_channel(self
, channel
, downloading
=None, \
352 include_description
=False, generate_thumbnails
=False):
354 Add episode from the given channel to this model.
355 Downloading should be a callback.
356 include_description should be a boolean value (True if description
357 is to be added to the episode row, or False if not)
360 self
._downloading
= downloading
361 self
._include
_description
= include_description
362 self
._generate
_thumbnails
= generate_thumbnails
364 self
._all
_episodes
_view
= getattr(channel
, 'ALL_EPISODES_PROXY', False)
366 old_length
= len(self
._episodes
)
367 self
._episodes
= channel
.get_all_episodes()
368 new_length
= len(self
._episodes
)
370 for i
in range(min(old_length
, new_length
)):
371 self
.emit('row-changed', (i
,), self
.create_tree_iter(i
))
373 if old_length
> new_length
:
374 for i
in reversed(range(new_length
, old_length
)):
375 self
.emit('row-deleted', (i
,))
376 elif old_length
< new_length
:
377 for i
in range(old_length
, new_length
):
378 self
.emit('row-inserted', (i
,), self
.create_tree_iter(i
))
381 def update_all(self
, downloading
=None, include_description
=False, \
382 generate_thumbnails
=False):
384 self
._downloading
= downloading
385 self
._include
_description
= include_description
386 self
._generate
_thumbnails
= generate_thumbnails
388 for i
in range(len(self
._episodes
)):
389 self
.emit('row-changed', (i
,), self
.create_tree_iter(i
))
391 def update_by_urls(self
, urls
, downloading
=None, include_description
=False, \
392 generate_thumbnails
=False):
394 self
._downloading
= downloading
395 self
._include
_description
= include_description
396 self
._generate
_thumbnails
= generate_thumbnails
398 for index
, episode
in enumerate(self
._episodes
):
399 if episode
.url
in urls
:
400 self
.emit('row-changed', (index
,), self
.create_tree_iter(index
))
402 def update_by_filter_iter(self
, iter, downloading
=None, \
403 include_description
=False, generate_thumbnails
=False):
404 # Convenience function for use by "outside" methods that use iters
405 # from the filtered episode list model (i.e. all UI things normally)
406 iter = self
._sorter
.convert_iter_to_child_iter(None, iter)
407 self
.update_by_iter(self
._filter
.convert_iter_to_child_iter(iter), \
408 downloading
, include_description
, generate_thumbnails
)
410 def update_by_iter(self
, iter, downloading
=None, include_description
=False, \
411 generate_thumbnails
=False, reload_from_db
=True):
413 self
._downloading
= downloading
414 self
._include
_description
= include_description
415 self
._generate
_thumbnails
= generate_thumbnails
417 index
= self
.get_user_data(iter)
418 episode
= self
._episodes
[index
]
420 episode
.reload_from_db()
422 self
.emit('row-changed', (index
,), self
.create_tree_iter(index
))
424 def _get_icon_from_image(self
,image_path
, icon_size
):
426 Load an local image file and transform it into an icon.
428 Return a pixbuf scaled to the desired size and may return None
429 if the icon creation is impossible (file not found etc).
431 if not os
.path
.exists(image_path
):
433 # load image from disc (code adapted from CoverDownloader
434 # except that no download is needed here)
435 loader
= gtk
.gdk
.PixbufLoader()
438 loader
.write(open(image_path
, 'rb').read())
440 pixbuf
= loader
.get_pixbuf()
442 log('Data error while loading image %s', image_path
, sender
=self
)
444 # Now scale the image with ratio (copied from _resize_pixbuf_keep_ratio)
446 if pixbuf
.get_width() > icon_size
:
447 f
= float(icon_size
)/pixbuf
.get_width()
448 (width
, height
) = (int(pixbuf
.get_width()*f
), int(pixbuf
.get_height()*f
))
449 pixbuf
= pixbuf
.scale_simple(width
, height
, gtk
.gdk
.INTERP_BILINEAR
)
451 if pixbuf
.get_height() > icon_size
:
452 f
= float(icon_size
)/pixbuf
.get_height()
453 (width
, height
) = (int(pixbuf
.get_width()*f
), int(pixbuf
.get_height()*f
))
454 pixbuf
= pixbuf
.scale_simple(width
, height
, gtk
.gdk
.INTERP_BILINEAR
)
458 def _get_tree_icon(self
, icon_name
, add_bullet
=False, \
459 add_padlock
=False, add_missing
=False, icon_size
=32, \
460 build_icon_from_file
= False):
462 Loads an icon from the current icon theme at the specified
463 size, suitable for display in a gtk.TreeView. Additional
464 emblems can be added on top of the icon.
466 Caching is used to speed up the icon lookup.
468 The `build_icon_from_file` argument indicates (when True) that
469 the icon has to be created on the fly from a given image
470 file. The `icon_name` argument is then interpreted as the path
471 to this file. Those specific icons will *not be cached*.
474 # Add all variables that modify the appearance of the icon, so
475 # our cache does not return the same icons for different requests
476 cache_id
= (icon_name
, add_bullet
, add_padlock
, add_missing
, icon_size
)
478 if cache_id
in self
._icon
_cache
:
479 return self
._icon
_cache
[cache_id
]
481 icon_theme
= gtk
.icon_theme_get_default()
484 if build_icon_from_file
:
485 icon
= self
._get
_icon
_from
_image
(icon_name
,icon_size
)
487 icon
= icon_theme
.load_icon(icon_name
, icon_size
, 0)
490 log('Missing icon in theme: %s', icon_name
, sender
=self
)
491 icon
= icon_theme
.load_icon(gtk
.STOCK_DIALOG_QUESTION
, \
494 log('Please install the GNOME icon theme.', sender
=self
)
495 icon
= gtk
.gdk
.Pixbuf(gtk
.gdk
.COLORSPACE_RGB
, \
496 True, 8, icon_size
, icon_size
)
498 if icon
and (add_bullet
or add_padlock
or add_missing
):
499 # We'll modify the icon, so use .copy()
503 # Desaturate the icon so it looks even more "missing"
504 icon
.saturate_and_pixelate(icon
, 0.0, False)
505 emblem
= icon_theme
.load_icon(self
.ICON_MISSING
, icon_size
/2, 0)
506 (width
, height
) = (emblem
.get_width(), emblem
.get_height())
507 xpos
= icon
.get_width() - width
508 ypos
= icon
.get_height() - height
509 emblem
.composite(icon
, xpos
, ypos
, width
, height
, xpos
, ypos
, 1, 1, gtk
.gdk
.INTERP_BILINEAR
, 255)
515 emblem
= icon_theme
.load_icon(self
.ICON_UNPLAYED
, icon_size
/2, 0)
516 (width
, height
) = (emblem
.get_width(), emblem
.get_height())
517 xpos
= icon
.get_width() - width
518 ypos
= icon
.get_height() - height
519 emblem
.composite(icon
, xpos
, ypos
, width
, height
, xpos
, ypos
, 1, 1, gtk
.gdk
.INTERP_BILINEAR
, 255)
525 emblem
= icon_theme
.load_icon(self
.ICON_LOCKED
, icon_size
/2, 0)
526 (width
, height
) = (emblem
.get_width(), emblem
.get_height())
527 emblem
.composite(icon
, 0, 0, width
, height
, 0, 0, 1, 1, gtk
.gdk
.INTERP_BILINEAR
, 255)
531 self
._icon
_cache
[cache_id
] = icon
536 class PodcastListModel(model
.PodcastListModel
):
537 def __init__(self
, *args
):
538 model
.PodcastListModel
.__init
__(self
, *args
)
540 normal_font
= style
.get_font_desc('SystemFont')
541 normal_color
= style
.get_color('DefaultTextColor')
542 normal
= (normal_font
.to_string(), normal_color
.to_string())
543 self
._normal
_markup
= '<span font_desc="%s" foreground="%s">%%s</span>' % normal
545 active_font
= style
.get_font_desc('SystemFont')
546 active_color
= style
.get_color('ActiveTextColor')
547 active
= (active_font
.to_string(), active_color
.to_string())
548 self
._active
_markup
= '<span font_desc="%s" foreground="%s">%%s</span>' % active
550 sub_font
= style
.get_font_desc('SmallSystemFont')
551 sub_color
= style
.get_color('SecondaryTextColor')
552 sub
= (sub_font
.to_string(), sub_color
.to_string())
553 sub
= '\n<span font_desc="%s" foreground="%s">%%s</span>' % sub
555 self
._unplayed
_markup
= self
._normal
_markup
+ sub
556 self
._active
_markup
+= sub
558 def _format_description(self
, channel
, total
, deleted
, \
559 new
, downloaded
, unplayed
):
560 title_markup
= cgi
.escape(channel
.title
)
561 if not unplayed
and not new
:
562 return self
._normal
_markup
% title_markup
564 new_text
= N_('%d new episode', '%d new episodes', new
) % new
565 unplayed_text
= N_('%d unplayed download', '%d unplayed downloads', unplayed
) % unplayed
567 return self
._active
_markup
% (title_markup
, ', '.join((new_text
, unplayed_text
)))
569 return self
._active
_markup
% (title_markup
, new_text
)
571 return self
._unplayed
_markup
% (title_markup
, unplayed_text
)