Fixing segfaults due to accessing seekbar after it is destroyed (plus an assertion)
[watersofshiloah] / src / windows / magazines.py
1 import logging
2
3 import gobject
4 import gtk
5
6 import hildonize
7 import util.go_utils as go_utils
8 import util.misc as misc_utils
9 import banners
10 import presenter
11 import stream_index
12
13 import windows
14
15
16 _moduleLogger = logging.getLogger(__name__)
17
18
19 class MagazinesWindow(windows._base.ListWindow):
20
21         def __init__(self, player, store, node):
22                 windows._base.ListWindow.__init__(self, player, store, node)
23                 self._window.set_title(self._node.title)
24
25         @classmethod
26         def _get_columns(cls):
27                 yield gobject.TYPE_PYOBJECT, None
28
29                 pixrenderer = gtk.CellRendererPixbuf()
30                 column = gtk.TreeViewColumn("Covers")
31                 column.pack_start(pixrenderer, expand=True)
32                 column.add_attribute(pixrenderer, "pixbuf", 1)
33                 yield gobject.TYPE_OBJECT, column
34
35                 textrenderer = gtk.CellRendererText()
36                 column = gtk.TreeViewColumn("Magazine")
37                 column.pack_start(textrenderer, expand=True)
38                 column.add_attribute(textrenderer, "text", 2)
39                 yield gobject.TYPE_STRING, column
40
41         def _refresh(self):
42                 windows._base.ListWindow._refresh(self)
43                 self._node.get_children(
44                         self._on_magazines,
45                         self._on_error,
46                 )
47
48         @misc_utils.log_exception(_moduleLogger)
49         def _on_magazines(self, programs):
50                 if self._isDestroyed:
51                         _moduleLogger.info("Download complete but window destroyed")
52                         return
53
54                 self._hide_loading()
55                 for i, programNode in enumerate(programs):
56                         program = programNode.get_properties()
57                         img = self._store.get_pixbuf_from_store(self._store.STORE_LOOKUP["nomagazineimage"])
58                         row = programNode, img, program["title"]
59                         self._model.append(row)
60
61                         programNode.get_children(self._create_on_issues(i), self._on_error)
62
63                 self._select_row()
64
65         def _create_on_issues(self, row):
66                 return lambda issues: self._on_issues(row, issues)
67
68         @misc_utils.log_exception(_moduleLogger)
69         def _on_issues(self, row, issues):
70                 for issue in issues:
71                         self._store.get_pixbuf_from_url(
72                                 issue.get_properties()["pictureURL"],
73                                 lambda pix: self._on_image(row, pix),
74                                 self._on_error,
75                         )
76                         break
77                 else:
78                         _moduleLogger.info("No issues for magazine %s" % row)
79
80         @misc_utils.log_exception(_moduleLogger)
81         def _on_image(self, row, pix):
82                 treeiter = self._model.iter_nth_child(None, row)
83                 self._model.set_value(treeiter, 1, pix)
84                 treeiter = self._model.iter_nth_child(None, row)
85                 self._model.row_changed((row, ), treeiter)
86
87         @misc_utils.log_exception(_moduleLogger)
88         def _on_error(self, exception):
89                 self._hide_loading()
90                 self._errorBanner.push_message(str(exception))
91
92         def _window_from_node(self, node):
93                 issuesWindow = MagazineIssuesWindow(self._player, self._store, node)
94                 issuesWindow.window.set_modal(True)
95                 issuesWindow.window.set_transient_for(self._window)
96                 issuesWindow.window.set_default_size(*self._window.get_size())
97                 issuesWindow.connect("quit", self._on_quit)
98                 issuesWindow.connect("home", self._on_home)
99                 issuesWindow.connect("jump-to", self._on_jump)
100                 issuesWindow.show()
101                 return issuesWindow
102
103         @misc_utils.log_exception(_moduleLogger)
104         def _on_row_activated(self, view, path, column):
105                 itr = self._model.get_iter(path)
106                 node = self._model.get_value(itr, 0)
107                 self._window_from_node(node)
108
109
110 gobject.type_register(MagazinesWindow)
111
112
113 class MagazineIssuesWindow(windows._base.ListWindow):
114
115         def __init__(self, player, store, node):
116                 windows._base.ListWindow.__init__(self, player, store, node)
117                 self._window.set_title(self._node.title)
118
119         @classmethod
120         def _get_columns(cls):
121                 yield gobject.TYPE_PYOBJECT, None
122
123                 pixrenderer = gtk.CellRendererPixbuf()
124                 column = gtk.TreeViewColumn("Covers")
125                 column.pack_start(pixrenderer, expand=True)
126                 column.add_attribute(pixrenderer, "pixbuf", 1)
127                 yield gobject.TYPE_OBJECT, column
128
129                 textrenderer = gtk.CellRendererText()
130                 column = gtk.TreeViewColumn("Issue")
131                 column.pack_start(textrenderer, expand=True)
132                 column.add_attribute(textrenderer, "text", 2)
133                 yield gobject.TYPE_STRING, column
134
135         def _refresh(self):
136                 windows._base.ListWindow._refresh(self)
137                 self._node.get_children(
138                         self._on_magazine_issues,
139                         self._on_error,
140                 )
141
142         @misc_utils.log_exception(_moduleLogger)
143         def _on_magazine_issues(self, programs):
144                 if self._isDestroyed:
145                         _moduleLogger.info("Download complete but window destroyed")
146                         return
147
148                 self._hide_loading()
149                 for programNode in programs:
150                         program = programNode.get_properties()
151                         img = self._store.get_pixbuf_from_store(self._store.STORE_LOOKUP["nomagazineimage"])
152                         row = programNode, img, program["title"]
153                         self._model.append(row)
154
155                         self._store.get_pixbuf_from_url(
156                                 program["pictureURL"],
157                                 self._create_on_image(programNode),
158                                 self._on_error,
159                         )
160
161                 self._select_row()
162
163         @misc_utils.log_exception(_moduleLogger)
164         def _on_error(self, exception):
165                 self._hide_loading()
166                 self._errorBanner.push_message(str(exception))
167
168         def _create_on_image(self, programNode):
169                 return lambda pix: self._on_image(programNode, pix)
170
171         @misc_utils.log_exception(_moduleLogger)
172         def _on_image(self, childNode, pix):
173                 for i, row in enumerate(self._model):
174                         if row[0] is childNode:
175                                 break
176                 else:
177                         raise RuntimeError("Could not find %r" % childNode)
178                 treeiter = self._model.iter_nth_child(None, i)
179                 self._model.set_value(treeiter, 1, pix)
180                 treeiter = self._model.iter_nth_child(None, i)
181                 self._model.row_changed((i, ), treeiter)
182
183         def _window_from_node(self, node):
184                 issuesWindow = MagazineArticlesWindow(self._player, self._store, node)
185                 issuesWindow.window.set_modal(True)
186                 issuesWindow.window.set_transient_for(self._window)
187                 issuesWindow.window.set_default_size(*self._window.get_size())
188                 issuesWindow.connect("quit", self._on_quit)
189                 issuesWindow.connect("home", self._on_home)
190                 issuesWindow.connect("jump-to", self._on_jump)
191                 issuesWindow.show()
192                 return issuesWindow
193
194         @misc_utils.log_exception(_moduleLogger)
195         def _on_row_activated(self, view, path, column):
196                 itr = self._model.get_iter(path)
197                 node = self._model.get_value(itr, 0)
198                 self._window_from_node(node)
199
200
201 gobject.type_register(MagazineIssuesWindow)
202
203
204 class MagazineArticlesWindow(windows._base.ListWindow):
205
206         def __init__(self, player, store, node):
207                 windows._base.ListWindow.__init__(self, player, store, node)
208                 self._window.set_title(self._node.title)
209
210         @classmethod
211         def _get_columns(cls):
212                 yield gobject.TYPE_PYOBJECT, None
213
214                 textrenderer = gtk.CellRendererText()
215                 column = gtk.TreeViewColumn("Article")
216                 column.pack_start(textrenderer, expand=True)
217                 column.add_attribute(textrenderer, "text", 1)
218                 yield gobject.TYPE_STRING, column
219
220         def _refresh(self):
221                 windows._base.ListWindow._refresh(self)
222                 self._node.get_children(
223                         self._on_magazine_articles,
224                         self._on_error,
225                 )
226
227         @misc_utils.log_exception(_moduleLogger)
228         def _on_magazine_articles(self, programs):
229                 if self._isDestroyed:
230                         _moduleLogger.info("Download complete but window destroyed")
231                         return
232
233                 self._hide_loading()
234                 for programNode in programs:
235                         program = programNode.get_properties()
236                         row = programNode, "%s\n%s" % (program["title"], program["author"])
237                         self._model.append(row)
238
239                 self._select_row()
240
241         @misc_utils.log_exception(_moduleLogger)
242         def _on_error(self, exception):
243                 self._hide_loading()
244                 self._errorBanner.push_message(str(exception))
245
246         def _window_from_node(self, node):
247                 issuesWindow = MagazineArticleWindow(self._player, self._store, node)
248                 issuesWindow.window.set_modal(True)
249                 issuesWindow.window.set_transient_for(self._window)
250                 issuesWindow.window.set_default_size(*self._window.get_size())
251                 issuesWindow.connect("quit", self._on_quit)
252                 issuesWindow.connect("home", self._on_home)
253                 issuesWindow.connect("jump-to", self._on_jump)
254                 issuesWindow.show()
255                 return issuesWindow
256
257         @misc_utils.log_exception(_moduleLogger)
258         def _on_row_activated(self, view, path, column):
259                 itr = self._model.get_iter(path)
260                 node = self._model.get_value(itr, 0)
261                 self._window_from_node(node)
262
263
264 gobject.type_register(MagazineArticlesWindow)
265
266
267 class MagazineArticleWindow(windows._base.BasicWindow):
268
269         def __init__(self, player, store, node):
270                 windows._base.BasicWindow.__init__(self, player, store)
271                 self._node = node
272                 self._playerNode = self._player.node
273                 self._nextSearch = None
274                 self._updateSeek = None
275
276                 self.connect_auto(self._player, "state-change", self._on_player_state_change)
277                 self.connect_auto(self._player, "title-change", self._on_player_title_change)
278                 self.connect_auto(self._player, "error", self._on_player_error)
279
280                 self._loadingBanner = banners.GenericBanner()
281
282                 self._presenter = presenter.StreamPresenter(self._store)
283                 self._presenter.set_context(
284                         self._store.STORE_LOOKUP["magazine_background"],
285                         self._node.title,
286                         self._node.subtitle,
287                 )
288                 self._presenterNavigation = presenter.NavigationBox()
289                 self._presenterNavigation.toplevel.add(self._presenter.toplevel)
290                 self._presenterNavigation.connect("action", self._on_nav_action)
291                 self._presenterNavigation.connect("navigating", self._on_navigating)
292
293                 self._seekbar = hildonize.create_seekbar()
294                 self._seekbar.connect("change-value", self._on_user_seek)
295
296                 self._layout.pack_start(self._loadingBanner.toplevel, False, False)
297                 self._layout.pack_start(self._presenterNavigation.toplevel, True, True)
298                 self._layout.pack_start(self._seekbar, False, False)
299
300                 self._window.set_title(self._node.title)
301
302         def show(self):
303                 windows._base.BasicWindow.show(self)
304                 self._window.show_all()
305                 self._errorBanner.toplevel.hide()
306                 self._loadingBanner.toplevel.hide()
307                 self._set_context(self._player.state)
308                 self._seekbar.hide()
309
310         def jump_to(self, node):
311                 assert self._node is node
312
313         @property
314         def _active(self):
315                 return self._playerNode is self._node
316
317         def _show_loading(self):
318                 animationPath = self._store.STORE_LOOKUP["loading"]
319                 animation = self._store.get_pixbuf_animation_from_store(animationPath)
320                 self._loadingBanner.show(animation, "Loading...")
321
322         def _hide_loading(self):
323                 self._loadingBanner.hide()
324
325         def _set_context(self, state):
326                 if state == self._player.STATE_PLAY:
327                         if self._active:
328                                 self._presenter.set_state(self._store.STORE_LOOKUP["pause"])
329                         else:
330                                 self._presenter.set_state(self._store.STORE_LOOKUP["play"])
331                 elif state == self._player.STATE_PAUSE:
332                         self._presenter.set_state(self._store.STORE_LOOKUP["play"])
333                 elif state == self._player.STATE_STOP:
334                         self._presenter.set_state(self._store.STORE_LOOKUP["play"])
335                 else:
336                         _moduleLogger.info("Unhandled player state %s" % state)
337
338         @misc_utils.log_exception(_moduleLogger)
339         def _on_user_seek(self, widget, scroll, value):
340                 self._player.seek(value / 100.0)
341
342         @misc_utils.log_exception(_moduleLogger)
343         def _on_player_update_seek(self):
344                 if self._isDestroyed:
345                         return False
346                 self._seekbar.set_value(self._player.percent_elapsed * 100)
347                 return True
348
349         @misc_utils.log_exception(_moduleLogger)
350         def _on_player_state_change(self, player, newState):
351                 if self._active and self._player.state == self._player.STATE_PLAY:
352                         self._seekbar.show()
353                         assert self._updateSeek is None
354                         self._updateSeek = go_utils.Timeout(self._on_player_update_seek, once=False)
355                         self._updateSeek.start(seconds=1)
356                 else:
357                         self._seekbar.hide()
358                         if self._updateSeek is not None:
359                                 self._updateSeek.cancel()
360                                 self._updateSeek = None
361
362                 if not self._presenterNavigation.is_active():
363                         self._set_context(newState)
364
365         @misc_utils.log_exception(_moduleLogger)
366         def _on_player_title_change(self, player, node):
367                 if not self._active or node in [None, self._node]:
368                         self._playerNode = player.node
369                         return
370                 self._playerNode = player.node
371                 self.emit("jump-to", node)
372                 self._window.destroy()
373
374         @misc_utils.log_exception(_moduleLogger)
375         def _on_player_error(self, player, err, debug):
376                 _moduleLogger.error("%r - %r" % (err, debug))
377
378         @misc_utils.log_exception(_moduleLogger)
379         def _on_navigating(self, widget, navState):
380                 if navState == "clicking":
381                         if self._player.state == self._player.STATE_PLAY:
382                                 if self._active:
383                                         imageName = "pause_pressed"
384                                 else:
385                                         imageName = "play_pressed"
386                         elif self._player.state == self._player.STATE_PAUSE:
387                                 imageName = "play_pressed"
388                         elif self._player.state == self._player.STATE_STOP:
389                                 imageName = "play_pressed"
390                         else:
391                                 _moduleLogger.info("Unhandled player state %s" % self._player.state)
392                 elif navState == "down":
393                         imageName = "home"
394                 elif navState == "up":
395                         if self._player.state == self._player.STATE_PLAY:
396                                 if self._active:
397                                         imageName = "pause"
398                                 else:
399                                         imageName = "play"
400                         elif self._player.state == self._player.STATE_PAUSE:
401                                 imageName = "play"
402                         elif self._player.state == self._player.STATE_STOP:
403                                 imageName = "play"
404                         else:
405                                 _moduleLogger.info("Unhandled player state %s" % self._player.state)
406                 elif navState == "left":
407                         imageName = "next"
408                 elif navState == "right":
409                         imageName = "prev"
410
411                 self._presenter.set_state(self._store.STORE_LOOKUP[imageName])
412
413         @misc_utils.log_exception(_moduleLogger)
414         def _on_nav_action(self, widget, navState):
415                 self._set_context(self._player.state)
416
417                 if navState == "clicking":
418                         if self._player.state == self._player.STATE_PLAY:
419                                 if self._active:
420                                         self._player.pause()
421                                 else:
422                                         self._player.set_piece_by_node(self._node)
423                                         self._player.play()
424                         elif self._player.state == self._player.STATE_PAUSE:
425                                 self._player.play()
426                         elif self._player.state == self._player.STATE_STOP:
427                                 self._player.set_piece_by_node(self._node)
428                                 self._player.play()
429                         else:
430                                 _moduleLogger.info("Unhandled player state %s" % self._player.state)
431                 elif navState == "down":
432                         self.emit("home")
433                         self._window.destroy()
434                 elif navState == "up":
435                         pass
436                 elif navState == "left":
437                         if self._active:
438                                 self._player.next()
439                         else:
440                                 assert self._nextSearch is None
441                                 self._nextSearch = stream_index.AsyncWalker(stream_index.get_next)
442                                 self._nextSearch.start(self._node, self._on_next_node, self._on_node_search_error)
443                 elif navState == "right":
444                         if self._active:
445                                 self._player.back()
446                         else:
447                                 assert self._nextSearch is None
448                                 self._nextSearch = stream_index.AsyncWalker(stream_index.get_previous)
449                                 self._nextSearch.start(self._node, self._on_next_node, self._on_node_search_error)
450
451         @misc_utils.log_exception(_moduleLogger)
452         def _on_next_node(self, node):
453                 self._nextSearch = None
454                 self.emit("jump-to", node)
455                 self._window.destroy()
456
457         @misc_utils.log_exception(_moduleLogger)
458         def _on_node_search_error(self, e):
459                 self._nextSearch = None
460                 self._errorBanner.push_message(str(e))
461
462
463 gobject.type_register(MagazineArticleWindow)