Hide/show buttons when offline/online
[seriesfinale:seriesfinale.git] / src / SeriesFinale / gui.py
1 # -*- coding: utf-8 -*-
2
3 ###########################################################################
4 #    SeriesFinale
5 #    Copyright (C) 2009 Joaquim Rocha <jrocha@igalia.com>
6 #
7 #    This program is free software: you can redistribute it and/or modify
8 #    it under the terms of the GNU General Public License as published by
9 #    the Free Software Foundation, either version 3 of the License, or
10 #    (at your option) any later version.
11 #
12 #    This program is distributed in the hope that it will be useful,
13 #    but WITHOUT ANY WARRANTY; without even the implied warranty of
14 #    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
15 #    GNU General Public License for more details.
16 #
17 #    You should have received a copy of the GNU General Public License
18 #    along with this program.  If not, see <http://www.gnu.org/licenses/>.
19 ###########################################################################
20
21 import hildon
22 import pygtk
23 pygtk.require('2.0')
24 import gtk
25 import gobject
26 import gettext
27 import locale
28 import pango
29 import os
30 import re
31 import time
32 from xml.sax import saxutils
33 from series import SeriesManager, Show, Episode
34 from lib import constants
35 from lib.connectionmanager import ConnectionManager
36 from lib.portrait import FremantleRotation
37 from lib.util import get_color
38 from settings import Settings
39 from asyncworker import AsyncWorker, AsyncItem
40
41 _ = gettext.gettext
42
43 gtk.gdk.threads_init()
44
45 class MainWindow(hildon.StackableWindow):
46
47     def __init__(self):
48         super(MainWindow, self).__init__()
49
50         # i18n
51         languages = []
52         lc, encoding = locale.getdefaultlocale()
53         if lc:
54             languages = [lc]
55         languages += constants.DEFAULT_LANGUAGES
56         gettext.bindtextdomain(constants.SF_COMPACT_NAME,
57                                constants.LOCALE_DIR)
58         gettext.textdomain(constants.SF_COMPACT_NAME)
59         language = gettext.translation(constants.SF_COMPACT_NAME,
60                                        constants.LOCALE_DIR,
61                                        languages = languages,
62                                        fallback = True)
63         _ = language.gettext
64
65         # Autorotation
66         self._rotation_manager = FremantleRotation(constants.SF_COMPACT_NAME,
67                                                   self)
68
69         self.connection_manager = ConnectionManager()
70         self.connection_manager.connect('connection-changed',
71                                         self._on_connection_changed)
72
73         self.series_manager = SeriesManager()
74         self.settings = Settings()
75         hildon.hildon_gtk_window_set_progress_indicator(self, True)
76         save_pid = AsyncItem(self.save_current_pid,())
77         load_conf_item = AsyncItem(self.settings.load,
78                                    (constants.SF_CONF_FILE,))
79         load_shows_item = AsyncItem(self.series_manager.load,
80                                     (constants.SF_DB_FILE,),
81                                     self._load_finished)
82         self.series_manager.connect('show-list-changed',
83                                     self._show_list_changed_cb)
84         self.series_manager.connect('get-full-show-complete',
85                                     self._get_show_complete_cb)
86         self.series_manager.connect('update-show-episodes-complete',
87                                     self._update_show_complete_cb)
88         self.series_manager.connect('update-shows-call-complete',
89                                     self._update_all_shows_complete_cb)
90         self.series_manager.connect('updated-show-art',
91                                     self._update_show_art)
92
93         self.request = AsyncWorker()
94         self.request.queue.put((0, save_pid))
95         self.request.queue.put((0, load_conf_item))
96         self.request.queue.put((0, load_shows_item))
97
98         old_pid = self.get_previous_pid()
99
100         if old_pid:
101             show_information(self, _("Waiting for previous SeriesFinale to finish...."))
102             gobject.timeout_add(2000,
103                                 self.run_after_pid_finish,
104                                 old_pid, self.request.start)
105         else:
106             self.request.start()
107
108         self.shows_view = ShowsSelectView()
109         self.shows_view.connect('row-activated', self._row_activated_cb)
110         self.set_title(constants.SF_NAME)
111         self.set_app_menu(self._create_menu())
112         self.live_search = LiveSearchEntry(self.shows_view.tree_model,
113                                            self.shows_view.tree_filter,
114                                            ShowListStore.SEARCH_COLUMN)
115         area = hildon.PannableArea()
116         area.add(self.shows_view)
117         box = gtk.VBox()
118         box.pack_start(area)
119         box.pack_end(self.live_search, False, False)
120         self.add(box)
121         box.show_all()
122         self.live_search.hide()
123
124         self.connect('delete-event', self._exit_cb)
125         self._update_delete_menu_visibility()
126
127         self.connect('key-press-event', self._key_press_event_cb)
128
129         self._have_deleted = False
130
131     def save_current_pid(self):
132         pidfile = open(constants.SF_PID_FILE, 'w')
133         pidfile.write('%s' % os.getpid())
134         pidfile.close()
135
136     def get_previous_pid(self):
137         if not os.path.isfile(constants.SF_PID_FILE):
138             return None
139
140         pidfile = open(constants.SF_PID_FILE, 'r')
141         pid = pidfile.readline()
142         pidfile.close()
143         if os.path.exists('/proc/%s' % pid):
144             return pid
145         else:
146             os.remove(constants.SF_PID_FILE)
147             return None
148     
149     def run_after_pid_finish(self, pid, callback):
150         if os.path.exists('/proc/%s' % pid):
151             return True
152
153         callback()
154         return False
155
156     def _load_finished(self, dummy_arg, error):
157         self.shows_view.set_shows(self.series_manager.series_list)
158         hildon.hildon_gtk_window_set_progress_indicator(self, False)
159         self.request = None
160         self._update_delete_menu_visibility()
161         self.shows_view.sort()
162         self.sort_by_name_filter.set_active(
163                                  self.settings.getConf(Settings.SHOWS_SORT) != \
164                                  Settings.RECENT_EPISODE)
165         self._applyRotation()
166         self.series_manager.auto_save(True)
167
168     def _create_menu(self):
169         menu = hildon.AppMenu()
170
171         button = hildon.GtkButton(gtk.HILDON_SIZE_FINGER_HEIGHT)
172         button.set_label(_('Add shows'))
173         button.connect('clicked', self._add_shows_cb)
174         menu.append(button)
175
176         self.sort_by_ep_filter = hildon.GtkRadioButton(
177                                         gtk.HILDON_SIZE_FINGER_HEIGHT)
178         self.sort_by_ep_filter.set_mode(False)
179         self.sort_by_ep_filter.set_label(_('Sort by ep. date'))
180         menu.add_filter(self.sort_by_ep_filter)
181         self.sort_by_name_filter = hildon.GtkRadioButton(
182                                           gtk.HILDON_SIZE_FINGER_HEIGHT,
183                                           group = self.sort_by_ep_filter)
184         self.sort_by_name_filter.set_mode(False)
185         self.sort_by_name_filter.set_label(_('Sort by name'))
186         menu.add_filter(self.sort_by_name_filter)
187         self.sort_by_name_filter.set_active(
188             self.settings.getConf(Settings.SHOWS_SORT) != \
189                 Settings.RECENT_EPISODE)
190         self.sort_by_ep_filter.connect('clicked',
191                                 lambda w: self.shows_view.sort_by_recent_date())
192         self.sort_by_name_filter.connect('clicked',
193                              lambda w: self.shows_view.sort_by_name_ascending())
194
195         self.delete_menu = hildon.GtkButton(gtk.HILDON_SIZE_FINGER_HEIGHT)
196         self.delete_menu.set_label(_('Delete shows'))
197         self.delete_menu.connect('clicked', self._delete_shows_cb)
198         menu.append(self.delete_menu)
199
200         self.update_all_menu = hildon.GtkButton(gtk.HILDON_SIZE_FINGER_HEIGHT)
201         self.update_all_menu.set_label(_('Update all'))
202         self.update_all_menu.connect('clicked', self._update_all_shows_cb)
203         menu.append(self.update_all_menu)
204
205         button = hildon.GtkButton(gtk.HILDON_SIZE_FINGER_HEIGHT)
206         button.set_label(_('Settings'))
207         button.connect('clicked', self._settings_menu_clicked_cb)
208         menu.append(button)
209
210         button = hildon.GtkButton(gtk.HILDON_SIZE_FINGER_HEIGHT)
211         button.set_label(_('About'))
212         button.connect('clicked', self._about_menu_clicked_cb)
213         menu.append(button)
214
215         menu.show_all()
216         return menu
217
218     def _add_shows_cb(self, button):
219         new_show_dialog = NewShowsDialog(self)
220         response = new_show_dialog.run()
221         new_show_dialog.destroy()
222         if response == NewShowsDialog.ADD_AUTOMATICALLY_RESPONSE:
223             self._launch_search_shows_dialog()
224         elif response == NewShowsDialog.ADD_MANUALLY_RESPONSE:
225             self._new_show_dialog()
226
227     def _delete_shows_cb(self, button):
228         delete_shows_view = ShowsDeleteView(self.series_manager)
229         delete_shows_view.shows_select_view.set_shows(self.series_manager.series_list)
230         delete_shows_view.show_all()
231         self._have_deleted = True
232
233     def _launch_search_shows_dialog(self):
234         search_dialog = SearchShowsDialog(self, self.series_manager)
235         response = search_dialog.run()
236         show = None
237         if response == gtk.RESPONSE_ACCEPT:
238             if search_dialog.chosen_show:
239                 hildon.hildon_gtk_window_set_progress_indicator(self, True)
240                 show_information(self,
241                                  _('Gathering show information. Please wait...'))
242                 if search_dialog.chosen_lang:
243                     self.series_manager.get_complete_show(search_dialog.chosen_show,
244                                                           search_dialog.chosen_lang)
245                 else:
246                     self.series_manager.get_complete_show(search_dialog.chosen_show)
247         search_dialog.destroy()
248
249     def _get_show_complete_cb(self, series_manager, show, error):
250         if error:
251             error_message = ''
252             if 'socket' in str(error).lower():
253                 error_message = '\n ' + _('Please verify your internet connection '
254                                           'is available')
255                 show_information(self,
256                                  _('An error occurred. %s') % error_message)
257         else:
258             self.shows_view.set_shows(self.series_manager.series_list)
259             self._update_delete_menu_visibility()
260         hildon.hildon_gtk_window_set_progress_indicator(self, False)
261
262     def _row_activated_cb(self, view, path, column):
263         show = self.shows_view.get_show_from_path(path)
264         seasons_view = SeasonsView(self.settings, self.series_manager, self.connection_manager, show)
265         seasons_view.connect('delete-event',
266                      lambda w, e:
267                         self.shows_view.update(show))
268         seasons_view.show_all()
269         self.live_search.hide()
270
271     def _new_show_dialog(self):
272         new_show_dialog = NewShowDialog(self)
273         response = new_show_dialog.run()
274         if response == gtk.RESPONSE_ACCEPT:
275             show_info = new_show_dialog.get_info()
276             show = Show(show_info['name'])
277             show.overview = show_info['overview']
278             show.genre = show_info['genre']
279             show.network = show_info['network']
280             show.rating = show_info['rating']
281             show.actors = show_info['actors']
282             self.series_manager.add_show(show)
283         new_show_dialog.destroy()
284
285     def _exit_cb(self, window, event):
286         if self.request:
287             self.request.stop()
288         hildon.hildon_gtk_window_set_progress_indicator(self, True)
289         # If the shows list is empty but the user hasn't deleted
290         # any, then we don't save in order to avoid overwriting
291         # the current db (for the shows list might be empty due
292         # to an error)
293         if not self.series_manager.series_list and not self._have_deleted:
294             gtk.main_quit()
295             return
296         self.series_manager.auto_save(False)
297
298         save_shows_item = AsyncItem(self.series_manager.save,
299                                (constants.SF_DB_FILE,))
300         save_conf_item = AsyncItem(self.settings.save,
301                                (constants.SF_CONF_FILE,),
302                                self._save_finished_cb)
303         async_worker = AsyncWorker()
304         async_worker.queue.put((0, save_shows_item))
305         async_worker.queue.put((0, save_conf_item))
306         async_worker.start()
307
308     def _save_finished_cb(self, dummy_arg, error):
309         hildon.hildon_gtk_window_set_progress_indicator(self, False)
310         gtk.main_quit()
311
312     def _show_list_changed_cb(self, series_manager):
313         self.shows_view.set_shows(self.series_manager.series_list)
314         self._update_delete_menu_visibility()
315         return False
316
317     def _update_delete_menu_visibility(self):
318         if not self.series_manager.series_list or self.request:
319             self.delete_menu.hide()
320             self.update_all_menu.hide()
321         else:
322             self.delete_menu.show()
323             self._on_connection_changed(self.connection_manager)
324
325     def _update_all_shows_cb(self, button):
326         hildon.hildon_gtk_window_set_progress_indicator(self, True)
327         self.request = self.series_manager.update_all_shows_episodes()
328         self.set_sensitive(False)
329         self._update_delete_menu_visibility()
330
331     def _update_all_shows_complete_cb(self, series_manager, show, error):
332         self._show_list_changed_cb(self.series_manager)
333         if self.request:
334             if error:
335                 show_information(self, _('Please verify your internet connection '
336                                          'is available'))
337             else:
338                 show_information(self, _('Finished updating the shows'))
339         self.request = None
340         self.set_sensitive(True)
341         self._update_delete_menu_visibility()
342         hildon.hildon_gtk_window_set_progress_indicator(self, False)
343
344     def _update_show_complete_cb(self, series_manager, show, error):
345         show_information(self, _('Updated "%s"') % show.name)
346
347     def _update_show_art(self, series_manager, show):
348         self.shows_view.update(show)
349
350     def _about_menu_clicked_cb(self, menu):
351         about_dialog = AboutDialog(self)
352         about_dialog.set_logo(constants.SF_ICON)
353         about_dialog.set_name(constants.SF_NAME)
354         about_dialog.set_version(constants.SF_VERSION)
355         about_dialog.set_comments(constants.SF_DESCRIPTION)
356         about_dialog.set_authors(constants.SF_AUTHORS)
357         about_dialog.set_copyright(constants.SF_COPYRIGHT)
358         about_dialog.set_license(saxutils.escape(constants.SF_LICENSE))
359         about_dialog.run()
360         about_dialog.destroy()
361
362     def _settings_menu_clicked_cb(self, menu):
363         settings_dialog = SettingsDialog(self)
364         response = settings_dialog.run()
365         settings_dialog.destroy()
366         if response == gtk.RESPONSE_ACCEPT:
367             self._applyRotation()
368
369     def _applyRotation(self):
370         configured_mode = self.settings.getConf(Settings.SCREEN_ROTATION)
371         modes = [self._rotation_manager.AUTOMATIC,
372                  self._rotation_manager.ALWAYS,
373                  self._rotation_manager.NEVER]
374         self._rotation_manager.set_mode(modes[configured_mode])
375
376     def _key_press_event_cb(self, window, event):
377         char = gtk.gdk.keyval_to_unicode(event.keyval)
378         if self.live_search.is_focus() or char == 0 or not chr(char).strip():
379             return
380         self.live_search.show()
381
382     def _on_connection_changed(self, connection_manager):
383         if connection_manager.is_online():
384             self.update_all_menu.show()
385         else:
386             self.update_all_menu.hide()
387
388 class DeleteView(hildon.StackableWindow):
389
390     def __init__(self,
391                  tree_view,
392                  toolbar_title = _('Delete'),
393                  button_label = _('Delete')):
394         super(DeleteView, self).__init__()
395         self.tree_view = tree_view
396         hildon.hildon_gtk_tree_view_set_ui_mode(self.tree_view, gtk.HILDON_UI_MODE_EDIT)
397         self.tree_view.get_selection().set_mode(gtk.SELECTION_MULTIPLE)
398         shows_area = hildon.PannableArea()
399         shows_area.add(self.tree_view)
400         self.add(shows_area)
401
402         self.toolbar = hildon.EditToolbar()
403         self.toolbar.set_label(toolbar_title)
404         self.toolbar.set_button_label(button_label)
405         self.toolbar.connect('arrow-clicked', lambda toolbar: self.destroy())
406         self.set_edit_toolbar(self.toolbar)
407
408         self.fullscreen()
409
410 class ShowsDeleteView(DeleteView):
411
412     def __init__(self, series_manager):
413         self.shows_select_view = ShowsSelectView()
414         super(ShowsDeleteView, self).__init__(self.shows_select_view,
415                                                _('Delete shows'),
416                                                _('Delete'))
417         self.series_manager = series_manager
418         self.toolbar.connect('button-clicked',
419                              self._button_clicked_cb)
420
421     def _button_clicked_cb(self, button):
422         selection = self.shows_select_view.get_selection()
423         selected_rows = selection.get_selected_rows()
424         model, paths = selected_rows
425         if not paths:
426             show_information(self, _('Please select one or more shows'))
427             return
428         for path in paths:
429             self.series_manager.delete_show(model[path][ShowListStore.SHOW_COLUMN])
430         self.destroy()
431
432 class ShowsSelectView(gtk.TreeView):
433
434     def __init__(self):
435         super(ShowsSelectView, self).__init__()
436         self.tree_model = ShowListStore()
437         show_image_renderer = gtk.CellRendererPixbuf()
438         column = gtk.TreeViewColumn('Image', show_image_renderer,
439                                     pixbuf = ShowListStore.IMAGE_COLUMN)
440         self.append_column(column)
441         show_renderer = gtk.CellRendererText()
442         show_renderer.set_property('ellipsize', pango.ELLIPSIZE_END)
443         column = gtk.TreeViewColumn('Name', show_renderer, markup = ShowListStore.INFO_COLUMN)
444         self.tree_filter = self.tree_model.filter_new()
445         self.set_model(self.tree_filter)
446         self.append_column(column)
447         self.sort()
448
449     def set_shows(self, shows):
450         self.tree_model.add_shows(shows)
451
452     def get_show_from_path(self, path):
453         return self.get_model()[path][ShowListStore.SHOW_COLUMN]
454
455     def sort_by_recent_date(self):
456         self.tree_model.set_sort_column_id(self.tree_model.NEXT_EPISODE_COLUMN,
457                                            gtk.SORT_ASCENDING)
458         Settings().setConf(Settings.SHOWS_SORT, Settings.RECENT_EPISODE)
459
460     def sort_by_name_ascending(self):
461         self.tree_model.set_sort_column_id(self.tree_model.INFO_COLUMN,
462                                            gtk.SORT_ASCENDING)
463         Settings().setConf(Settings.SHOWS_SORT, Settings.ASCENDING_ORDER)
464
465     def update(self, show = None):
466         if self.tree_model:
467             self.tree_model.update(show)
468             self.sort()
469
470     def sort(self):
471         shows_sort_order = Settings().getConf(Settings.SHOWS_SORT)
472         if shows_sort_order == Settings.RECENT_EPISODE:
473             self.sort_by_recent_date()
474         else:
475             self.sort_by_name_ascending()
476
477 class ShowListStore(gtk.ListStore):
478
479     IMAGE_COLUMN = 0
480     INFO_COLUMN = 1
481     SHOW_COLUMN = 2
482     SEARCH_COLUMN = 3
483     NEXT_EPISODE_COLUMN = 4
484
485     def __init__(self):
486         super(ShowListStore, self).__init__(gtk.gdk.Pixbuf, str, gobject.TYPE_PYOBJECT, str, gobject.TYPE_PYOBJECT)
487         self.cached_pixbufs = {}
488         self.set_sort_func(self.NEXT_EPISODE_COLUMN, self._sort_func)
489
490     def add_shows(self, shows):
491         self.clear()
492         for show in shows:
493             row = {self.IMAGE_COLUMN: None,
494                    self.INFO_COLUMN: saxutils.escape(show.name),
495                    self.SHOW_COLUMN: show,
496                    self.SEARCH_COLUMN: saxutils.escape(show.name).lower(),
497                    self.NEXT_EPISODE_COLUMN: None
498                   }
499             self.append(row.values())
500         self.update(None)
501
502     def update(self, show):
503         iter = self.get_iter_first()
504         while iter:
505             current_show = self.get_value(iter, self.SHOW_COLUMN)
506             if show is None or show == current_show:
507                 self._update_iter(iter)
508             iter = self.iter_next(iter)
509
510     def _update_iter(self, iter):
511         show = self.get_value(iter, self.SHOW_COLUMN)
512         pixbuf = self.get_value(iter, self.IMAGE_COLUMN)
513         info = show.get_episodes_info()
514         info_markup = show.get_info_markup(info)
515         self.set_value(iter, self.INFO_COLUMN, info_markup)
516         self.set_value(iter, self.NEXT_EPISODE_COLUMN,
517                        info['next_episode'])
518         self.set_value(iter, self.SEARCH_COLUMN, show.name.lower())
519         if pixbuf_is_cover(pixbuf):
520             return
521         if show.image and os.path.isfile(show.image):
522             pixbuf = self.cached_pixbufs.get(show.image)
523             if not pixbuf:
524                 try:
525                     pixbuf = gtk.gdk.pixbuf_new_from_file_at_size(show.image,
526                                                           constants.IMAGE_WIDTH,
527                                                           constants.IMAGE_HEIGHT)
528                 except:
529                     pixbuf = get_placeholder_pixbuf()
530             self.cached_pixbufs[show.image] = pixbuf
531             self.set_value(iter, self.IMAGE_COLUMN, pixbuf)
532         elif show.downloading_show_image:
533             pixbuf = get_downloading_pixbuf()
534             self.set_value(iter, self.IMAGE_COLUMN, pixbuf)
535         else:
536             pixbuf = get_placeholder_pixbuf()
537             self.set_value(iter, self.IMAGE_COLUMN, pixbuf)
538
539     def _sort_func(self, model, iter1, iter2):
540         episode1 = model.get_value(iter1, self.NEXT_EPISODE_COLUMN)
541         episode2 = model.get_value(iter2, self.NEXT_EPISODE_COLUMN)
542         if not episode1:
543             if episode2:
544                 return 1
545             return 0
546         if not episode2:
547             if episode1:
548                 return -1
549         most_recent = (episode1 or episode2).get_most_recent(episode2)
550         if not most_recent:
551             return 0
552         if episode1 == most_recent:
553             return -1
554         return 1
555
556 class SeasonsView(hildon.StackableWindow):
557
558     def __init__(self, settings, series_manager, connection_manager, show):
559         super(SeasonsView, self).__init__()
560         self.set_title(show.name)
561
562         self.settings = settings
563
564         self.series_manager = series_manager
565         self.series_manager.connect('update-show-episodes-complete',
566                                     self._update_show_episodes_complete_cb)
567         self.series_manager.connect('updated-show-art',
568                                     self._update_show_art)
569
570         self.connection_manager = connection_manager
571         self.connection_manager.connect('connection-changed',
572                                         self._on_connection_changed)
573         self.show = show
574         self.set_app_menu(self._create_menu())
575         self.set_title(show.name)
576
577         self.seasons_select_view = SeasonSelectView(self.show)
578         seasons = self.show.get_seasons()
579         self.seasons_select_view.set_seasons(seasons)
580         self.seasons_select_view.connect('row-activated', self._row_activated_cb)
581         self.connect('delete-event', self._delete_event_cb)
582
583         seasons_area = hildon.PannableArea()
584         seasons_area.add(self.seasons_select_view)
585         self.add(seasons_area)
586
587         self.request = None
588         self._update_menu_visibility()
589
590     def _delete_event_cb(self, window, event):
591         if self.request:
592             self.request.stop()
593             self.request = None
594         return False
595
596     def _row_activated_cb(self, view, path, column):
597         season = self.seasons_select_view.get_season_from_path(path)
598         episodes_view = EpisodesView(self.settings, self.show, season)
599         episodes_view.connect('delete-event', self._update_series_list_cb)
600         episodes_view.connect('episode-list-changed', self._update_series_list_cb)
601         episodes_view.show_all()
602
603     def _update_series_list_cb(self, widget, event = None):
604         seasons = self.show.get_seasons();
605         self.seasons_select_view.set_seasons(seasons)
606         self._update_menu_visibility()
607
608     def _create_menu(self):
609         menu = hildon.AppMenu()
610
611         button = hildon.GtkButton(gtk.HILDON_SIZE_FINGER_HEIGHT)
612         button.set_label(_('Info'))
613         button.connect('clicked', self._show_info_cb)
614         menu.append(button)
615
616         button = hildon.GtkButton(gtk.HILDON_SIZE_FINGER_HEIGHT)
617         button.set_label(_('Edit info'))
618         button.connect('clicked', self._edit_show_info)
619         menu.append(button)
620
621         self.update_menu = None
622         if str(self.show.thetvdb_id) != '-1':
623             self.update_menu = hildon.GtkButton(gtk.HILDON_SIZE_FINGER_HEIGHT)
624             self.update_menu.set_label(_('Update show'))
625             self.update_menu.connect('clicked', self._update_series_cb)
626             menu.append(self.update_menu)
627
628         button = hildon.GtkButton(gtk.HILDON_SIZE_FINGER_HEIGHT)
629         button.set_label(_('Delete season'))
630         button.connect('clicked', self._delete_seasons_cb)
631         menu.append(button)
632
633         button = hildon.GtkButton(gtk.HILDON_SIZE_FINGER_HEIGHT)
634         button.set_label(_('New episode'))
635         button.connect('clicked', self._new_episode_cb)
636         menu.append(button)
637
638         menu.show_all()
639         return menu
640
641     def _update_menu_visibility(self):
642         if not self.update_menu:
643             return
644         if self.request or not self.show.get_seasons():
645             self.update_menu.hide()
646         else:
647             self.update_menu.show()
648             self._on_connection_changed(self.connection_manager)
649
650     def _update_series_cb(self, button):
651         self.request = self.series_manager.update_show_episodes(self.show)
652         hildon.hildon_gtk_window_set_progress_indicator(self, True)
653         self.set_sensitive(False)
654         show_information(self, _('Updating show. Please wait...'))
655         self._update_menu_visibility()
656
657     def _show_info_cb(self, button):
658         dialog = gtk.Dialog(parent = self)
659         dialog.set_title(_('Show details'))
660         infotextview = InfoTextView()
661         infotextview.set_title(self.show.name)
662         infotextview.add_field (self.show.overview)
663         infotextview.add_field ('\n')
664         infotextview.add_field (self.show.genre, _('Genre'))
665         infotextview.add_field (self.show.network, _('Network'))
666         infotextview.add_field (self.show.actors, _('Actors'))
667         infotextview.add_field (self.show.rating, _('Rating'))
668         info_area = hildon.PannableArea()
669         info_area.add_with_viewport(infotextview)
670         info_area.set_size_request_policy(hildon.SIZE_REQUEST_CHILDREN)
671         dialog.vbox.add(info_area)
672         dialog.vbox.show_all()
673         dialog.run()
674         dialog.destroy()
675
676     def _edit_show_info(self, button):
677         edit_series_dialog = EditShowsDialog(self, self.show)
678         response = edit_series_dialog.run()
679         info = edit_series_dialog.get_info()
680         edit_series_dialog.destroy()
681         if response == gtk.RESPONSE_ACCEPT:
682             self.show.name = info['name']
683             self.show.overview = info['overview']
684             self.show.genre = info['genre']
685             self.show.network = info['network']
686             self.show.rating = info['rating']
687             self.show.actors = info['actors']
688             self.series_manager.updated()
689         self.set_title(self.show.name)
690
691     def _new_episode_cb(self, button):
692         new_episode_dialog = NewEpisodeDialog(self,
693                                               self.show)
694         response = new_episode_dialog.run()
695         if response == gtk.RESPONSE_ACCEPT:
696             episode_info = new_episode_dialog.get_info()
697             episode = Episode(episode_info['name'],
698                               self.show,
699                               episode_info['number'])
700             episode.overview = episode_info['overview']
701             episode.season_number = episode_info['season']
702             episode.episode_number = episode_info['number']
703             episode.director = episode_info['director']
704             episode.writer = episode_info['writer']
705             episode.rating = episode_info['rating']
706             episode.air_date = episode_info['air_date']
707             episode.guest_stars = episode_info['guest_stars']
708             self.show.update_episode_list([episode])
709             seasons = self.show.get_seasons()
710             self.seasons_select_view.set_seasons(seasons)
711         new_episode_dialog.destroy()
712
713     def _update_show_episodes_complete_cb(self, series_manager, show, error):
714         if error and self.request:
715             error_message = ''
716             if 'socket' in str(error).lower():
717                 error_message = '\n ' + _('Please verify your internet connection '
718                                           'is available')
719             show_information(self, error_message)
720         elif show == self.show:
721             seasons = self.show.get_seasons()
722             model = self.seasons_select_view.get_model()
723             if model:
724                 model.clear()
725                 self.seasons_select_view.set_seasons(seasons)
726         hildon.hildon_gtk_window_set_progress_indicator(self, False)
727         self.set_sensitive(True)
728         self.request = None
729         self._update_menu_visibility()
730
731     def _update_show_art(self, series_manager, show):
732         if show == self.show:
733             self.seasons_select_view.update()
734
735     def _delete_seasons_cb(self, button):
736         seasons_delete_view = SeasonsDeleteView(self.series_manager,
737                                                 self.seasons_select_view)
738         seasons = self.show.get_seasons()
739         seasons_delete_view.show_all()
740
741     def _on_connection_changed(self, connection_manager):
742         if connection_manager.is_online():
743             self.update_menu.show()
744         else:
745             self.update_menu.hide()
746
747 class SeasonsDeleteView(DeleteView):
748
749     def __init__(self, series_manager, seasons_select_view):
750         self.seasons_select_view = SeasonSelectView(seasons_select_view.show)
751         self.seasons_select_view.set_model(seasons_select_view.get_model())
752         super(SeasonsDeleteView, self).__init__(self.seasons_select_view,
753                                                _('Delete seasons'),
754                                                _('Delete'))
755         self.series_manager = series_manager
756         self.toolbar.connect('button-clicked',
757                              self._button_clicked_cb)
758
759     def _button_clicked_cb(self, button):
760         selection = self.seasons_select_view.get_selection()
761         selected_rows = selection.get_selected_rows()
762         model, paths = selected_rows
763         if not paths:
764             show_information(self, _('Please select one or more seasons'))
765             return
766         for path in paths:
767             self.seasons_select_view.show.delete_season(
768                  model[path][SeasonListStore.SEASON_COLUMN])
769             model.remove(model.get_iter(path))
770         self.destroy()
771
772 class SeasonListStore(gtk.ListStore):
773
774     IMAGE_COLUMN = 0
775     INFO_COLUMN = 1
776     SEASON_COLUMN = 2
777
778     def __init__(self, show):
779         super(SeasonListStore, self).__init__(gtk.gdk.Pixbuf,
780                                               str,
781                                               gobject.TYPE_PYOBJECT)
782         self.show = show
783
784     def add(self, season_list):
785         self.clear()
786         for season in season_list:
787             if season == '0':
788                 name = _('Special')
789             else:
790                 name = _('Season %s') % season
791             row = {self.IMAGE_COLUMN: None,
792                    self.INFO_COLUMN: name,
793                    self.SEASON_COLUMN: season,
794                   }
795             self.append(row.values())
796         self.update()
797
798     def update(self):
799         iter = self.get_iter_first()
800         while iter:
801             self._update_iter(iter)
802             iter = self.iter_next(iter)
803
804     def _update_iter(self, iter):
805         season = self.get_value(iter, self.SEASON_COLUMN)
806         info = self.show.get_season_info_markup(season)
807         self.set_value(iter, self.INFO_COLUMN, info)
808         pixbuf = self.get_value(iter, self.IMAGE_COLUMN)
809         image = self.show.season_images.get(season)
810         if pixbuf_is_cover(pixbuf):
811             return
812         if image and os.path.isfile(image):
813             try:
814                 pixbuf = gtk.gdk.pixbuf_new_from_file_at_size(image,
815                                                           constants.IMAGE_WIDTH,
816                                                           constants.IMAGE_HEIGHT)
817             except:
818                 pixbuf = get_placeholder_pixbuf()
819             self.set_value(iter, self.IMAGE_COLUMN, pixbuf)
820         elif self.show.downloading_season_image:
821             pixbuf = get_downloading_pixbuf()
822             self.set_value(iter, self.IMAGE_COLUMN, pixbuf)
823         else:
824             pixbuf = get_placeholder_pixbuf()
825             self.set_value(iter, self.IMAGE_COLUMN, pixbuf)
826
827 class SeasonSelectView(gtk.TreeView):
828
829     def __init__(self, show):
830         super(SeasonSelectView, self).__init__()
831         self.show = show
832         model = SeasonListStore(self.show)
833         season_image_renderer = gtk.CellRendererPixbuf()
834         column = gtk.TreeViewColumn('Image', season_image_renderer, pixbuf = model.IMAGE_COLUMN)
835         self.append_column(column)
836         season_renderer = gtk.CellRendererText()
837         season_renderer.set_property('ellipsize', pango.ELLIPSIZE_END)
838         column = gtk.TreeViewColumn('Name', season_renderer, markup = model.INFO_COLUMN)
839         self.set_model(model)
840         self.append_column(column)
841
842     def set_seasons(self, season_list):
843         model = self.get_model()
844         model.add(season_list)
845
846     def get_season_from_path(self, path):
847         model = self.get_model()
848         iter = model.get_iter(path)
849         season = model.get_value(iter, model.SEASON_COLUMN)
850         return season
851
852     def update(self):
853         model = self.get_model()
854         if model:
855             model.update()
856
857 class NewShowDialog(gtk.Dialog):
858
859     def __init__(self, parent):
860         super(NewShowDialog, self).__init__(parent = parent,
861                                              buttons = (gtk.STOCK_ADD,
862                                                         gtk.RESPONSE_ACCEPT))
863
864         self.set_title(_('Edit show'))
865
866         self.show_name = hildon.Entry(gtk.HILDON_SIZE_FINGER_HEIGHT)
867         self.show_overview = hildon.TextView()
868         self.show_overview.set_placeholder(_('Overview'))
869         self.show_overview.set_wrap_mode(gtk.WRAP_WORD)
870         self.show_genre = hildon.Entry(gtk.HILDON_SIZE_FINGER_HEIGHT)
871         self.show_network = hildon.Entry(gtk.HILDON_SIZE_FINGER_HEIGHT)
872         self.show_rating = hildon.Entry(gtk.HILDON_SIZE_FINGER_HEIGHT)
873         self.show_actors = hildon.Entry(gtk.HILDON_SIZE_FINGER_HEIGHT)
874
875         contents = gtk.VBox(False, 0)
876
877         row = gtk.HBox(False, 12)
878         row.pack_start(gtk.Label(_('Name:')), False, False, 0)
879         row.pack_start(self.show_name, True, True, 0)
880         contents.pack_start(row, False, False, 0)
881         contents.pack_start(self.show_overview, False, False, 0)
882
883         fields = [(_('Genre:'), self.show_genre),
884                   (_('Network:'), self.show_network),
885                   (_('Rating:'), self.show_rating),
886                   (_('Actors:'), self.show_actors),
887                  ]
888         size_group = gtk.SizeGroup(gtk.SIZE_GROUP_BOTH)
889         for text, widget in fields:
890             row = gtk.HBox(False, 12)
891             label = gtk.Label(text)
892             size_group.add_widget(label)
893             row.pack_start(label, False, False, 0)
894             row.pack_start(widget, True, True, 0)
895             contents.pack_start(row, False, False, 0)
896
897         contents_area = hildon.PannableArea()
898         contents_area.add_with_viewport(contents)
899         contents_area.set_size_request_policy(hildon.SIZE_REQUEST_CHILDREN)
900
901         self.vbox.add(contents_area)
902         self.vbox.show_all()
903
904     def get_info(self):
905         buffer = self.show_overview.get_buffer()
906         start_iter = buffer.get_start_iter()
907         end_iter = buffer.get_end_iter()
908         overview_text = buffer.get_text(start_iter, end_iter)
909         info = {'name': self.show_name.get_text(),
910                 'overview': overview_text,
911                 'genre': self.show_genre.get_text(),
912                 'network': self.show_network.get_text(),
913                 'rating': self.show_rating.get_text(),
914                 'actors': self.show_actors.get_text()}
915         return info
916
917 class EditShowsDialog(NewShowDialog):
918
919     def __init__(self, parent, show):
920         super(EditShowsDialog, self).__init__(parent)
921
922         self.show_name.set_text(show.name)
923         self.show_overview.get_buffer().set_text(show.overview)
924         self.show_genre.set_text(str(show.genre))
925         self.show_network.set_text(show.network)
926         self.show_rating.set_text(show.rating)
927         self.show_actors.set_text(str(show.actors))
928
929 class NewEpisodeDialog(gtk.Dialog):
930
931     def __init__(self, parent, show):
932         super(NewEpisodeDialog, self).__init__(parent = parent,
933                                                buttons = (gtk.STOCK_ADD,
934                                                           gtk.RESPONSE_ACCEPT))
935
936         self.set_title(_('New episode'))
937
938         self.episode_name = hildon.Entry(gtk.HILDON_SIZE_FINGER_HEIGHT)
939         self.episode_overview = hildon.TextView()
940         self.episode_overview.set_placeholder(_('Overview'))
941         self.episode_overview.set_wrap_mode(gtk.WRAP_WORD)
942
943         self.episode_number = hildon.PickerButton(gtk.HILDON_SIZE_FINGER_HEIGHT,
944                                                   hildon.BUTTON_ARRANGEMENT_VERTICAL)
945         selector = hildon.TouchSelectorEntry(text = True)
946         self.episode_number.set_title(_('Number:'))
947         for i in xrange(20):
948             selector.append_text(str(i + 1))
949         self.episode_number.set_selector(selector)
950         self.episode_number.set_active(0)
951
952         self.episode_season = hildon.PickerButton(gtk.HILDON_SIZE_FINGER_HEIGHT,
953                                                   hildon.BUTTON_ARRANGEMENT_VERTICAL)
954         selector = hildon.TouchSelectorEntry(text = True)
955         self.episode_season.set_title(_('Season:'))
956         seasons = show.get_seasons()
957         for season in seasons:
958             selector.append_text(season)
959         self.episode_season.set_selector(selector)
960         if seasons:
961             self.episode_season.set_active(len(seasons) - 1)
962         else:
963             selector.append_text('1')
964             self.episode_season.set_active(0)
965
966         self.episode_director = hildon.Entry(gtk.HILDON_SIZE_FINGER_HEIGHT)
967         self.episode_writer = hildon.Entry(gtk.HILDON_SIZE_FINGER_HEIGHT)
968         self.episode_air_date = hildon.Entry(gtk.HILDON_SIZE_FINGER_HEIGHT)
969         self.episode_rating = hildon.Entry(gtk.HILDON_SIZE_FINGER_HEIGHT)
970         self.episode_guest_stars = hildon.Entry(gtk.HILDON_SIZE_FINGER_HEIGHT)
971
972         contents = gtk.VBox(False, 0)
973
974         row = gtk.HBox(False, 12)
975         row.pack_start(gtk.Label(_('Name:')), False, False, 0)
976         row.pack_start(self.episode_name, True, True, 0)
977         contents.pack_start(row, False, False, 0)
978         contents.pack_start(self.episode_overview, False, False, 0)
979         row = gtk.HBox(False, 12)
980         row.add(self.episode_season)
981         row.add(self.episode_number)
982         contents.pack_start(row, False, False, 0)
983
984         fields = [(_('Director:'), self.episode_director),
985                   (_('Writer:'), self.episode_writer),
986                   (_('Original air date:'), self.episode_air_date),
987                   (_('Rating:'), self.episode_rating),
988                   (_('Guest stars:'), self.episode_guest_stars),
989                  ]
990         size_group = gtk.SizeGroup(gtk.SIZE_GROUP_BOTH)
991         for text, widget in fields:
992             row = gtk.HBox(False, 12)
993             label = gtk.Label(text)
994             size_group.add_widget(label)
995             row.pack_start(label, False, False, 0)
996             row.pack_start(widget, True, True, 0)
997             contents.pack_start(row, False, False, 0)
998
999         contents_area = hildon.PannableArea()
1000         contents_area.add_with_viewport(contents)
1001         contents_area.set_size_request_policy(hildon.SIZE_REQUEST_CHILDREN)
1002
1003         self.vbox.add(contents_area)
1004         self.vbox.show_all()
1005
1006     def get_info(self):
1007         buffer = self.episode_overview.get_buffer()
1008         start_iter = buffer.get_start_iter()
1009         end_iter = buffer.get_end_iter()
1010         overview_text = buffer.get_text(start_iter, end_iter)
1011         info = {'name': self.episode_name.get_text(),
1012                 'overview': overview_text,
1013                 'season': self.episode_season.get_selector().get_entry().get_text(),
1014                 'number': self.episode_number.get_selector().get_entry().get_text(),
1015                 'director': self.episode_director.get_text(),
1016                 'writer': self.episode_writer.get_text(),
1017                 'air_date': self.episode_air_date.get_text(),
1018                 'rating': self.episode_rating.get_text(),
1019                 'guest_stars': self.episode_guest_stars.get_text()}
1020         return info
1021
1022 class EditEpisodeDialog(NewEpisodeDialog):
1023
1024     def __init__(self, parent, episode):
1025         super(EditEpisodeDialog, self).__init__(parent, episode.show)
1026
1027         self.episode_name.set_text(episode.name)
1028         self.episode_overview.get_buffer().set_text(episode.overview)
1029         self.episode_season.get_selector().get_entry().set_text(episode.season_number)
1030         self.episode_number.get_selector().get_entry().set_text(str(episode.episode_number))
1031         self.episode_director.set_text(episode.director)
1032         self.episode_writer.set_text(str(episode.writer))
1033         self.episode_air_date.set_text(str(episode.air_date))
1034         self.episode_rating.set_text(episode.rating)
1035         self.episode_guest_stars.set_text(str(episode.guest_stars))
1036
1037 class EpisodesView(hildon.StackableWindow):
1038
1039     EPISODES_LIST_CHANGED_SIGNAL = 'episode-list-changed'
1040
1041     __gsignals__ = {EPISODES_LIST_CHANGED_SIGNAL: (gobject.SIGNAL_RUN_LAST,
1042                                                    gobject.TYPE_NONE,
1043                                                    ()),
1044                    }
1045
1046     def __init__(self, settings, show, season_number = None):
1047         super(EpisodesView, self).__init__()
1048
1049         self.settings = Settings()
1050         self.series_manager = SeriesManager()
1051
1052         self.show = show
1053         self.season_number = season_number
1054         self.set_title(self.show.name)
1055         self.episodes_check_view = EpisodesCheckView()
1056         self.episodes_check_view.set_episodes(self.show.get_episodes_by_season(self.season_number))
1057         self.episodes_check_view.watched_renderer.connect('toggled',
1058                                                           self._watched_renderer_toggled_cb,
1059                                                           self.episodes_check_view.get_model())
1060         self.episodes_check_view.connect('row-activated', self._row_activated_cb)
1061
1062         episodes_area = hildon.PannableArea()
1063         episodes_area.add(self.episodes_check_view)
1064         self.add(episodes_area)
1065         self.set_app_menu(self._create_menu())
1066         if self.settings.getConf(self.settings.EPISODES_ORDER_CONF_NAME) == \
1067            self.settings.ASCENDING_ORDER:
1068             self._sort_ascending_cb(None)
1069         else:
1070             self._sort_descending_cb(None)
1071
1072     def _create_menu(self):
1073         menu = hildon.AppMenu()
1074
1075         button_asc = hildon.GtkRadioButton(gtk.HILDON_SIZE_FINGER_HEIGHT)
1076         button_asc.set_mode(False)
1077         button_asc.set_label(_('A-Z'))
1078         menu.add_filter(button_asc)
1079         button_desc = hildon.GtkRadioButton(gtk.HILDON_SIZE_FINGER_HEIGHT, group = button_asc)
1080         button_desc.set_mode(False)
1081         button_desc.set_label(_('Z-A'))
1082         menu.add_filter(button_desc)
1083         if self.settings.getConf(Settings.EPISODES_ORDER_CONF_NAME) == \
1084                 Settings.ASCENDING_ORDER:
1085             button_asc.set_active(True)
1086         else:
1087             button_desc.set_active(True)
1088         button_asc.connect('clicked', self._sort_ascending_cb)
1089         button_desc.connect('clicked', self._sort_descending_cb)
1090
1091         button = hildon.GtkButton(gtk.HILDON_SIZE_FINGER_HEIGHT)
1092         button.set_label(_('Mark all'))
1093         button.connect('clicked', self._select_all_cb)
1094         menu.append(button)
1095
1096         button = hildon.GtkButton(gtk.HILDON_SIZE_FINGER_HEIGHT)
1097         button.set_label(_('Mark none'))
1098         button.connect('clicked', self._select_none_cb)
1099         menu.append(button)
1100
1101         button = hildon.GtkButton(gtk.HILDON_SIZE_FINGER_HEIGHT)
1102         button.set_label(_('Delete episodes'))
1103         button.connect('clicked', self._delete_episodes_cb)
1104         menu.append(button)
1105
1106         menu.show_all()
1107         return menu
1108
1109     def _delete_episodes_cb(self, button):
1110         delete_episodes_view = EpisodesDeleteView(self.show)
1111         episodes = self.show.get_episodes_by_season(self.season_number)
1112         delete_episodes_view.episodes_select_view.set_episodes(episodes)
1113         delete_episodes_view.toolbar.connect('button-clicked',
1114                                              self._update_episodes_list_cb)
1115         delete_episodes_view.show_all()
1116
1117     def _select_all_cb(self, button):
1118         self.episodes_check_view.select_all()
1119
1120     def _select_none_cb(self, button):
1121         self.episodes_check_view.select_none()
1122
1123     def _row_activated_cb(self, view, path, column):
1124         episode = self.episodes_check_view.get_episode_from_path(path)
1125         if self.episodes_check_view.get_column(EpisodeListStore.INFO_COLUMN) == column:
1126             episodes_view = EpisodeView(episode)
1127             episodes_view.connect('delete-event', self._update_episodes_list_cb)
1128             episodes_view.show_all()
1129
1130     def _update_episodes_list_cb(self, widget, event = None):
1131         self.emit(self.EPISODES_LIST_CHANGED_SIGNAL)
1132         episodes = self.show.get_episodes_by_season(self.season_number)
1133         if episodes:
1134             self.episodes_check_view.set_episodes(episodes)
1135         else:
1136             self.destroy()
1137         return False
1138
1139     def _watched_renderer_toggled_cb(self, renderer, path, model):
1140         episode = self.episodes_check_view.get_episode_from_path(path)
1141         episode.watched = not episode.watched
1142         episode.updated()
1143         model[path][model.CHECK_COLUMN] = episode.watched
1144         model.update_iter(model.get_iter(path))
1145
1146     def _sort_ascending_cb(self, button):
1147         self.episodes_check_view.sort_ascending()
1148         self.settings.setConf(self.settings.EPISODES_ORDER_CONF_NAME,
1149                               self.settings.ASCENDING_ORDER)
1150
1151     def _sort_descending_cb(self, button):
1152         self.episodes_check_view.sort_descending()
1153         self.settings.setConf(self.settings.EPISODES_ORDER_CONF_NAME,
1154                               self.settings.DESCENDING_ORDER)
1155
1156 class EpisodeListStore(gtk.ListStore):
1157     CHECK_COLUMN = 0
1158     INFO_COLUMN = 1
1159     EPISODE_COLUMN = 2
1160
1161     def __init__(self):
1162         EpisodeListStore.CHECK_COLUMN = Settings().getConf(Settings.EPISODES_CHECK_POSITION)
1163         EpisodeListStore.INFO_COLUMN = 1 - EpisodeListStore.CHECK_COLUMN
1164         types = {self.CHECK_COLUMN: bool, self.INFO_COLUMN: str,
1165                  self.EPISODE_COLUMN: gobject.TYPE_PYOBJECT}
1166         super(EpisodeListStore, self).__init__(*types.values())
1167
1168     def add(self, episode_list):
1169         self.clear()
1170         for episode in episode_list:
1171             name = str(episode)
1172             row = {self.CHECK_COLUMN: episode.watched,
1173                    self.INFO_COLUMN: name,
1174                    self.EPISODE_COLUMN: episode}
1175             self.append(row.values())
1176         self.update()
1177
1178
1179     def update(self):
1180         iter = self.get_iter_first()
1181         while iter:
1182             self.update_iter(iter)
1183             iter = self.iter_next(iter)
1184
1185     def update_iter(self, iter):
1186         episode = self.get_value(iter, self.EPISODE_COLUMN)
1187         info = episode.get_info_markup()
1188         self.set_value(iter, self.INFO_COLUMN, info)
1189
1190 class EpisodesCheckView(gtk.TreeView):
1191
1192     def __init__(self):
1193         super(EpisodesCheckView, self).__init__()
1194         model = EpisodeListStore()
1195         episode_renderer = gtk.CellRendererText()
1196         episode_renderer.set_property('ellipsize', pango.ELLIPSIZE_END)
1197         column = gtk.TreeViewColumn('Name', episode_renderer, markup = model.INFO_COLUMN)
1198         column.set_property('expand', True)
1199         self.append_column(column)
1200         self.watched_renderer = gtk.CellRendererToggle()
1201         self.watched_renderer.set_property('width', 100)
1202         self.watched_renderer.set_property('activatable', True)
1203         column = gtk.TreeViewColumn('Watched', self.watched_renderer)
1204         column.add_attribute(self.watched_renderer, "active", model.CHECK_COLUMN)
1205         self.insert_column(column, model.CHECK_COLUMN)
1206         self.set_model(model)
1207         self.get_model().set_sort_func(2, self._sort_func)
1208
1209     def _sort_func(self, model, iter1, iter2):
1210         episode1 = model.get_value(iter1, model.EPISODE_COLUMN)
1211         episode2 = model.get_value(iter2, model.EPISODE_COLUMN)
1212         if episode1 == None or episode2 == None:
1213             return 0
1214         if episode1.episode_number < episode2.episode_number:
1215             return -1
1216         return 1
1217
1218     def set_episodes(self, episode_list):
1219         model = self.get_model()
1220         model.add(episode_list)
1221
1222     def get_episode_from_path(self, path):
1223         model = self.get_model()
1224         iter = model.get_iter(path)
1225         episode = model.get_value(iter, model.EPISODE_COLUMN)
1226         return episode
1227
1228     def sort_descending(self):
1229         model = self.get_model()
1230         model.set_sort_column_id(model.EPISODE_COLUMN,
1231                                  gtk.SORT_DESCENDING)
1232
1233     def sort_ascending(self):
1234         model = self.get_model()
1235         model.set_sort_column_id(model.EPISODE_COLUMN,
1236                                  gtk.SORT_ASCENDING)
1237
1238     def select_all(self):
1239         self._set_episodes_selection(True)
1240         self.get_model().update()
1241
1242     def select_none(self):
1243         self._set_episodes_selection(False)
1244         self.get_model().update()
1245
1246     def _set_episodes_selection(self, mark):
1247         model = self.get_model()
1248         for path in model or []:
1249             path[model.CHECK_COLUMN] = \
1250                 path[model.EPISODE_COLUMN].watched = mark
1251             path[model.EPISODE_COLUMN].updated()
1252
1253 class EpisodeView(hildon.StackableWindow):
1254
1255     def __init__(self, episode):
1256         super(EpisodeView, self).__init__()
1257         self.episode = episode
1258
1259         contents_area = hildon.PannableArea()
1260         contents_area.connect('horizontal-movement',
1261                               self._horizontal_movement_cb)
1262         contents = gtk.VBox(False, 0)
1263         contents_area.add_with_viewport(contents)
1264
1265         self.infotextview = InfoTextView()
1266         self._update_info_text_view()
1267         contents.add(self.infotextview)
1268
1269         self.set_app_menu(self._create_menu())
1270
1271         self.add(contents_area)
1272
1273     def _update_info_text_view(self):
1274         self.infotextview.clear()
1275         self.infotextview.set_title('%(number)s - %(name)s' % {'name': self.episode.name,
1276                                                                'number': self.episode.get_episode_show_number()})
1277         self.infotextview.add_field(self.episode.overview)
1278         self.infotextview.add_field('\n')
1279         self.infotextview.add_field(self.episode.get_air_date_text(),
1280                                     _('Original air date'))
1281         self.infotextview.add_field(self.episode.director, _('Director'))
1282         self.infotextview.add_field(self.episode.writer, _('Writer'))
1283         self.infotextview.add_field(self.episode.guest_stars, _('Guest stars'))
1284         self.infotextview.add_field(self.episode.rating, _('Rating'))
1285         self.set_title(self.episode.name)
1286
1287     def _create_menu(self):
1288         menu = hildon.AppMenu()
1289
1290         button = hildon.GtkButton(gtk.HILDON_SIZE_FINGER_HEIGHT)
1291         button.set_label(_('Edit info'))
1292         button.connect('clicked', self._edit_episode_cb)
1293         menu.append(button)
1294
1295         menu.show_all()
1296         return menu
1297
1298     def _edit_episode_cb(self, button):
1299         edit_episode_dialog = EditEpisodeDialog(self,
1300                                                self.episode)
1301         response = edit_episode_dialog.run()
1302         if response == gtk.RESPONSE_ACCEPT:
1303             episode_info = edit_episode_dialog.get_info()
1304             self.episode.name = episode_info['name']
1305             self.episode.overview = episode_info['overview']
1306             self.episode.season_number = episode_info['season']
1307             self.episode.episode_number = episode_info['number']
1308             self.episode.air_date = episode_info['air_date']
1309             self.episode.director = episode_info['director']
1310             self.episode.writer = episode_info['writer']
1311             self.episode.rating = episode_info['rating']
1312             self.episode.guest_stars = episode_info['guest_stars']
1313             self.episode.updated()
1314             self._update_info_text_view()
1315         edit_episode_dialog.destroy()
1316
1317     def _horizontal_movement_cb(self, pannable_area, direction,
1318                                 initial_x, initial_y):
1319         if direction == hildon.MOVEMENT_LEFT:
1320             episode = self.episode.show.get_next_episode(self.episode)
1321         else:
1322             episode = self.episode.show.get_previous_episode(self.episode)
1323         if episode:
1324             self.episode = episode
1325             self._update_info_text_view()
1326
1327 class EpisodesDeleteView(DeleteView):
1328
1329     def __init__(self, show):
1330         self.episodes_select_view = EpisodesSelectView()
1331         super(EpisodesDeleteView, self).__init__(self.episodes_select_view,
1332                                                  _('Delete episodes'),
1333                                                  _('Delete'))
1334         self.show = show
1335         self.toolbar.connect('button-clicked',
1336                              self._button_clicked_cb)
1337
1338     def _button_clicked_cb(self, button):
1339         selection = self.episodes_select_view.get_selection()
1340         selected_rows = selection.get_selected_rows()
1341         model, paths = selected_rows
1342         if not paths:
1343             show_information(self, _('Please select one or more episodes'))
1344             return
1345         for path in paths:
1346             self.show.delete_episode(model[path][1])
1347         self.destroy()
1348
1349 class EpisodesSelectView(gtk.TreeView):
1350
1351     def __init__(self):
1352         super(EpisodesSelectView, self).__init__()
1353         model = gtk.ListStore(str, gobject.TYPE_PYOBJECT)
1354         column = gtk.TreeViewColumn('Name', gtk.CellRendererText(), text = 0)
1355         self.append_column(column)
1356         self.set_model(model)
1357
1358     def set_episodes(self, episode_list):
1359         model = self.get_model()
1360         model.clear()
1361         for episode in episode_list:
1362             name = str(episode)
1363             model.append([name, episode])
1364
1365     def get_episode_from_path(self, path):
1366         model = self.get_model()
1367         iter = model.get_iter(path)
1368         episode = model.get_value(iter, 1)
1369         return episode
1370
1371 class InfoTextView(hildon.TextView):
1372
1373     def __init__(self):
1374         super(InfoTextView, self).__init__()
1375
1376         buffer = gtk.TextBuffer()
1377         self.iter = buffer.get_start_iter()
1378
1379         self.set_buffer(buffer)
1380         self.set_wrap_mode(gtk.WRAP_WORD)
1381         self.set_editable(False)
1382         self.set_cursor_visible(False)
1383
1384     def set_title(self, title):
1385         if not title:
1386             return
1387         title_tag = gtk.TextTag()
1388         title_tag.set_property('weight', pango.WEIGHT_BOLD)
1389         title_tag.set_property('size', pango.units_from_double(24.0))
1390         title_tag.set_property('underline-set', True)
1391         tag_table = self.get_buffer().get_tag_table()
1392         tag_table.add(title_tag)
1393         self.get_buffer().insert_with_tags(self.iter, str(title) + '\n', title_tag)
1394
1395     def add_field(self, contents, label = None):
1396         if not contents:
1397             return
1398         if label:
1399             contents = _('\n%(label)s: %(contents)s') % {'label': label,
1400                                                          'contents': contents,
1401                                                         }
1402         self.get_buffer().insert(self.iter, contents)
1403
1404     def clear(self):
1405         buffer = self.get_buffer()
1406         if not buffer:
1407             return
1408         buffer.delete(buffer.get_start_iter(), buffer.get_end_iter())
1409         self.iter = buffer.get_start_iter()
1410
1411 class LiveSearchEntry(gtk.HBox):
1412
1413     def __init__(self, tree_model, tree_filter, filter_column = 0):
1414         super(LiveSearchEntry, self).__init__()
1415         self.tree_model = tree_model
1416         self.tree_filter = tree_filter
1417         if not self.tree_model or not self.tree_filter:
1418             return
1419         self.filter_column = filter_column
1420         self.tree_filter.set_visible_func(self._tree_filter_func)
1421         self.entry = hildon.Entry(gtk.HILDON_SIZE_FINGER_HEIGHT)
1422         self.entry.set_input_mode(gtk.HILDON_GTK_INPUT_MODE_FULL)
1423         self.entry.show()
1424         self.entry.connect('changed',
1425                            self._entry_changed_cb)
1426         self.cancel_button = hildon.GtkButton(gtk.HILDON_SIZE_FINGER_HEIGHT)
1427         image = gtk.image_new_from_icon_name('general_close',
1428                                              gtk.ICON_SIZE_LARGE_TOOLBAR)
1429         if image:
1430             self.cancel_button.set_image(image)
1431         else:
1432             self.cancel_button.set_label(_('Cancel'))
1433         self.cancel_button.set_size_request(self.cancel_button.get_size_request()[1], -1)
1434         self.cancel_button.show()
1435         self.cancel_button.connect('clicked', self._cancel_button_clicked_cb)
1436         self.pack_start(self.entry)
1437         self.pack_start(self.cancel_button, False, False)
1438
1439     def _cancel_button_clicked_cb(self, button):
1440         self.hide()
1441         self.entry.set_text('')
1442
1443     def _tree_filter_func(self, model, iter):
1444         info = model.get_value(iter, self.filter_column) or ''
1445         text_to_filter = self.entry.get_text().strip()
1446         if not text_to_filter:
1447             return True
1448         expr = re.compile('(.*\s+)*(%s)' % re.escape(text_to_filter.lower()))
1449         if expr.match(info):
1450             return True
1451         return False
1452
1453     def _entry_changed_cb(self, entry):
1454         self.tree_filter.refilter()
1455         if not entry.get_text():
1456             self.hide()
1457
1458     def show(self):
1459         gtk.HBox.show(self)
1460         self.entry.grab_focus()
1461
1462     def hide(self):
1463         gtk.HBox.hide(self)
1464         self.entry.set_text('')
1465
1466 class NewShowsDialog(gtk.Dialog):
1467
1468     ADD_AUTOMATICALLY_RESPONSE = 1 << 0
1469     ADD_MANUALLY_RESPONSE      = 1 << 1
1470
1471     def __init__(self, parent):
1472         super(NewShowsDialog, self).__init__(parent = parent)
1473         self.set_title(_('Add shows'))
1474         contents = gtk.HBox(True, 0)
1475         self.search_shows_button = hildon.GtkButton(gtk.HILDON_SIZE_FINGER_HEIGHT)
1476         self.search_shows_button.set_label(_('Search shows'))
1477         self.search_shows_button.connect('clicked', self._button_clicked_cb)
1478         self.manual_add_button = hildon.GtkButton(gtk.HILDON_SIZE_FINGER_HEIGHT)
1479         self.manual_add_button.set_label(_('Add manually'))
1480         self.manual_add_button.connect('clicked', self._button_clicked_cb)
1481         contents.add(self.search_shows_button)
1482         contents.add(self.manual_add_button)
1483         self.vbox.add(contents)
1484         self.vbox.show_all()
1485         parent.connection_manager.connect('connection-changed',
1486                                           self._on_connection_changed)
1487         self._on_connection_changed(parent.connection_manager)
1488
1489     def _button_clicked_cb(self, button):
1490         if button == self.search_shows_button:
1491             self.response(self.ADD_AUTOMATICALLY_RESPONSE)
1492         elif button == self.manual_add_button:
1493             self.response(self.ADD_MANUALLY_RESPONSE)
1494
1495     def _on_connection_changed(self, connection_manager):
1496         if connection_manager.is_online():
1497             self.search_shows_button.show()
1498         else:
1499             self.search_shows_button.hide()
1500
1501 class FoundShowListStore(gtk.ListStore):
1502
1503     NAME_COLUMN = 0
1504     MARKUP_COLUMN = 1
1505
1506     def __init__(self):
1507         super(FoundShowListStore, self).__init__(str, str)
1508
1509     def add_shows(self, shows):
1510         self.clear()
1511         for name in shows:
1512             markup_name = saxutils.escape(str(name))
1513             if self.series_manager.get_show_by_name(name):
1514                 row = {self.NAME_COLUMN: name,
1515                        self.MARKUP_COLUMN: '<span foreground="%s">%s</span>' % \
1516                                            (get_color(constants.ACTIVE_TEXT_COLOR), markup_name)}
1517             else:
1518                 row = {self.NAME_COLUMN: name,
1519                        self.MARKUP_COLUMN: '<span>%s</span>' % markup_name}
1520             self.append(row.values())
1521
1522 class SearchShowsDialog(gtk.Dialog):
1523
1524     def __init__(self, parent, series_manager):
1525         super(SearchShowsDialog, self).__init__(parent = parent)
1526         self.set_title(_('Search shows'))
1527
1528         self.series_manager = series_manager
1529         self.series_manager.connect('search-shows-complete', self._search_shows_complete_cb)
1530
1531         self.connect('response', self._response_cb)
1532
1533         self.chosen_show = None
1534         self.chosen_lang = None
1535
1536         self.shows_view = hildon.GtkTreeView(gtk.HILDON_UI_MODE_EDIT)
1537         model = FoundShowListStore()
1538         model.series_manager = series_manager
1539         show_renderer = gtk.CellRendererText()
1540         show_renderer.set_property('ellipsize', pango.ELLIPSIZE_END)
1541         column = gtk.TreeViewColumn('Name', show_renderer, markup = model.MARKUP_COLUMN)
1542         self.shows_view.set_model(model)
1543         self.shows_view.append_column(column)
1544
1545         self.search_entry = hildon.Entry(gtk.HILDON_SIZE_FINGER_HEIGHT)
1546         self.search_entry.connect('changed', self._search_entry_changed_cb)
1547         self.search_entry.connect('activate', self._search_entry_activated_cb)
1548         self.search_button = hildon.GtkButton(gtk.HILDON_SIZE_FINGER_HEIGHT)
1549         self.search_button.set_label(_('Search'))
1550         self.search_button.connect('clicked', self._search_button_clicked)
1551         self.search_button.set_sensitive(False)
1552         self.search_button.set_size_request(150, -1)
1553         search_contents = gtk.HBox(False, 0)
1554         search_contents.pack_start(self.search_entry, True, True, 0)
1555         search_contents.pack_start(self.search_button, False, False, 0)
1556         self.vbox.pack_start(search_contents, False, False, 0)
1557
1558         self.lang_store = gtk.ListStore(str, str);
1559         for langid, langdesc in self.series_manager.get_languages().iteritems():
1560             self.lang_store.append([langid, langdesc])
1561         lang_button = hildon.PickerButton(gtk.HILDON_SIZE_AUTO, hildon.BUTTON_ARRANGEMENT_VERTICAL)
1562         lang_button.set_title(_('Language'))
1563         self.lang_selector = hildon.TouchSelector()
1564         lang_column = self.lang_selector.append_column(self.lang_store, gtk.CellRendererText(), text=1)
1565         lang_column.set_property("text-column", 1)
1566         self.lang_selector.set_column_selection_mode(hildon.TOUCH_SELECTOR_SELECTION_MODE_SINGLE)
1567         lang_button.set_selector(self.lang_selector)
1568         try:
1569             self.lang_selector.set_active(0, self.series_manager.get_languages().keys().index(self.series_manager.get_default_language()))
1570         except ValueError:
1571             pass
1572
1573         shows_area = hildon.PannableArea()
1574         shows_area.add(self.shows_view)
1575         shows_area.set_size_request_policy(hildon.SIZE_REQUEST_CHILDREN)
1576         self.vbox.add(shows_area)
1577
1578         self.action_area.pack_start(lang_button, True, True, 0)
1579         self.ok_button = self.add_button(gtk.STOCK_OK, gtk.RESPONSE_ACCEPT)
1580         self.ok_button.set_sensitive(False)
1581         self.action_area.show_all()
1582
1583         self.vbox.show_all()
1584         self.set_size_request(-1, 400)
1585
1586     def _search_entry_changed_cb(self, entry):
1587         enable = self.search_entry.get_text().strip()
1588         self.search_button.set_sensitive(bool(enable))
1589
1590     def _search_entry_activated_cb(self, entry):
1591         self._search()
1592
1593     def _search_button_clicked(self, button):
1594         self._search()
1595
1596     def _search(self):
1597         self._set_controls_sensitive(False)
1598         hildon.hildon_gtk_window_set_progress_indicator(self, True)
1599         search_terms = self.search_entry.get_text()
1600         if not self.search_entry.get_text():
1601             return
1602         selected_row = self.lang_selector.get_active(0)
1603         if selected_row < 0:
1604             self.series_manager.search_shows(search_terms)
1605         else:
1606             lang = self.lang_store[selected_row][0]
1607             self.series_manager.search_shows(search_terms, lang)
1608
1609     def _search_shows_complete_cb(self, series_manager, shows, error):
1610         if error:
1611             error_message = ''
1612             if 'socket' in str(error).lower():
1613                 error_message = '\n ' + _('Please verify your internet connection '
1614                                           'is available')
1615                 show_information(self, _('An error occurred. %s') % error_message)
1616         else:
1617             model = self.shows_view.get_model()
1618             if not model:
1619                 return
1620             model.clear()
1621             if shows:
1622                 model.add_shows(shows)
1623                 self.ok_button.set_sensitive(True)
1624             else:
1625                 self.ok_button.set_sensitive(False)
1626         hildon.hildon_gtk_window_set_progress_indicator(self, False)
1627         self._set_controls_sensitive(True)
1628
1629     def _set_controls_sensitive(self, sensitive):
1630         self.search_entry.set_sensitive(sensitive)
1631         self.search_button.set_sensitive(sensitive)
1632
1633     def _response_cb(self, dialog, response):
1634         selection = self.shows_view.get_selection()
1635         model, paths = selection.get_selected_rows()
1636         for path in paths:
1637             iter = model.get_iter(path)
1638             text = model.get_value(iter, model.NAME_COLUMN)
1639             self.chosen_show = text
1640         selected_lang = self.lang_selector.get_active(0)
1641         if selected_lang >= 0:
1642             self.chosen_lang = self.lang_store[self.lang_selector.get_active(0)][0]
1643
1644 class AboutDialog(gtk.Dialog):
1645
1646     PADDING = 5
1647
1648     def __init__(self, parent):
1649         super(AboutDialog, self).__init__(parent = parent,
1650                                        flags = gtk.DIALOG_DESTROY_WITH_PARENT)
1651         self._logo = gtk.Image()
1652         self._name = ''
1653         self._name_label = gtk.Label()
1654         self._version = ''
1655         self._comments_label = gtk.Label()
1656         self._copyright_label = gtk.Label()
1657         self._license_label = gtk.Label()
1658         _license_alignment = gtk.Alignment(0, 0, 0, 1)
1659         _license_alignment.add(self._license_label)
1660         self._license_label.set_line_wrap(True)
1661
1662         self._writers_caption = gtk.Label()
1663         self._writers_caption.set_markup('<b>%s</b>' % _('Authors:'))
1664         _writers_caption = gtk.Alignment()
1665         _writers_caption.add(self._writers_caption)
1666         self._writers_label = gtk.Label()
1667         self._writers_contents = gtk.VBox(False, 0)
1668         self._writers_contents.pack_start(_writers_caption)
1669         _writers_alignment = gtk.Alignment(0.2, 0, 0, 1)
1670         _writers_alignment.add(self._writers_label)
1671         self._writers_contents.pack_start(_writers_alignment)
1672
1673         _contents = gtk.VBox(False, 0)
1674         _contents.pack_start(self._logo, False, False, self.PADDING)
1675         _contents.pack_start(self._name_label, False, False, self.PADDING)
1676         _contents.pack_start(self._comments_label, False, False, self.PADDING)
1677         _contents.pack_start(self._copyright_label, False, False, self.PADDING)
1678         _contents.pack_start(self._writers_contents, False, False, self.PADDING)
1679         _contents.pack_start(_license_alignment, False, False, self.PADDING)
1680
1681         _contents_area = hildon.PannableArea()
1682         _contents_area.add_with_viewport(_contents)
1683         _contents_area.set_size_request_policy(hildon.SIZE_REQUEST_CHILDREN)
1684         self.vbox.add(_contents_area)
1685         self.vbox.show_all()
1686         self._writers_contents.hide()
1687
1688     def set_logo(self, logo_path):
1689         self._logo.set_from_file(logo_path)
1690
1691     def set_name(self, name):
1692         self._name = name
1693         self.set_version(self._version)
1694         self.set_title(_('About %s') % self._name)
1695
1696     def _set_name_label(self, name):
1697         self._name_label.set_markup('<big>%s</big>' % name)
1698
1699     def set_version(self, version):
1700         self._version = version
1701         self._set_name_label('%s %s' % (self._name, self._version))
1702
1703     def set_comments(self, comments):
1704         self._comments_label.set_text(comments)
1705
1706     def set_copyright(self, copyright):
1707         self._copyright_label.set_markup('<small>%s</small>' % copyright)
1708
1709     def set_license(self, license):
1710         self._license_label.set_markup('<b>%s</b>\n<small>%s</small>' % \
1711                                        (_('License:'), license))
1712
1713     def set_authors(self, authors_list):
1714         authors = '\n'.join(authors_list)
1715         self._writers_label.set_text(authors)
1716         self._writers_contents.show_all()
1717
1718 class SettingsDialog(gtk.Dialog):
1719
1720     def __init__(self, parent):
1721         super(SettingsDialog, self).__init__(parent = parent,
1722                                          title = _('Settings'),
1723                                          flags = gtk.DIALOG_DESTROY_WITH_PARENT,
1724                                          buttons = (gtk.STOCK_SAVE,
1725                                                     gtk.RESPONSE_ACCEPT))
1726         self.settings = Settings()
1727         self.vbox.pack_start(self._create_screen_rotation_settings())
1728         self.vbox.pack_start(self._create_shows_settings())
1729         self.vbox.pack_start(self._create_episodes_check_settings())
1730         self.vbox.show_all()
1731
1732     def _create_screen_rotation_settings(self):
1733         picker_button = hildon.PickerButton(gtk.HILDON_SIZE_FINGER_HEIGHT,
1734                                             hildon.BUTTON_ARRANGEMENT_HORIZONTAL)
1735         picker_button.set_alignment(0, 0.5, 0, 1)
1736         picker_button.set_done_button_text(_('Done'))
1737         selector = hildon.TouchSelector(text = True)
1738         picker_button.set_title(_('Screen rotation:'))
1739         modes = [_('Automatic'), _('Portrait'), _('Landscape')]
1740         for mode in modes:
1741             selector.append_text(mode)
1742         picker_button.set_selector(selector)
1743         picker_button.set_active(self.settings.getConf(Settings.SCREEN_ROTATION))
1744         picker_button.connect('value-changed',
1745                               self._screen_rotation_picker_button_changed_cb)
1746         return picker_button
1747
1748     def _create_shows_settings(self):
1749         check_button = hildon.CheckButton(gtk.HILDON_SIZE_FINGER_HEIGHT)
1750         check_button.set_label(_('Add special seasons'))
1751         check_button.set_active(self.settings.getConf(Settings.ADD_SPECIAL_SEASONS))
1752         check_button.connect('toggled',
1753                              self._special_seasons_check_button_toggled_cb)
1754         return check_button
1755
1756     def _create_episodes_check_settings(self):
1757         picker_button = hildon.PickerButton(gtk.HILDON_SIZE_FINGER_HEIGHT,
1758                                             hildon.BUTTON_ARRANGEMENT_HORIZONTAL)
1759         picker_button.set_title(_('Episodes check position:'))
1760         picker_button.set_alignment(0, 0.5, 0, 1)
1761         selector = hildon.TouchSelector(text = True)
1762         selector.append_text(_('Left'))
1763         selector.append_text(_('Right'))
1764         picker_button.set_selector(selector)
1765         picker_button.set_active(self.settings.getConf(Settings.EPISODES_CHECK_POSITION))
1766         picker_button.connect('value-changed',
1767                               self._episodes_check_picker_button_changed_cb)
1768         return picker_button
1769
1770     def _special_seasons_check_button_toggled_cb(self, button):
1771         self.settings.setConf(Settings.ADD_SPECIAL_SEASONS, button.get_active())
1772
1773     def _screen_rotation_picker_button_changed_cb(self, button):
1774         self.settings.setConf(Settings.SCREEN_ROTATION, button.get_active())
1775
1776     def _episodes_check_picker_button_changed_cb(self, button):
1777         self.settings.setConf(Settings.EPISODES_CHECK_POSITION,
1778                               button.get_active())
1779
1780 def show_information(parent, message):
1781     hildon.hildon_banner_show_information(parent,
1782                                           '',
1783                                           message)
1784
1785 def pixbuf_is_cover(pixbuf):
1786     if pixbuf:
1787         return not bool(pixbuf.get_data('is_placeholder'))
1788     return False
1789
1790 def get_downloading_pixbuf():
1791     pixbuf = gtk.gdk.pixbuf_new_from_file_at_size(constants.DOWNLOADING_IMAGE,
1792                                                   constants.IMAGE_WIDTH,
1793                                                   constants.IMAGE_HEIGHT)
1794     pixbuf.set_data('is_placeholder', True)
1795     return pixbuf
1796
1797 def get_placeholder_pixbuf():
1798     pixbuf = gtk.gdk.pixbuf_new_from_file_at_size(constants.PLACEHOLDER_IMAGE,
1799                                                 constants.IMAGE_WIDTH,
1800                                                 constants.IMAGE_HEIGHT)
1801     pixbuf.set_data('is_placeholder', True)
1802     return pixbuf