Fixing issues of leaking window
[watersofshiloah] / src / windows.py
1 # @todo Add icons to buttons/rows to indicate that the currently playing track is coming from that
2
3 import ConfigParser
4 import datetime
5 import logging
6
7 import gobject
8 import gtk
9
10 import constants
11 import hildonize
12 import util.misc as misc_utils
13 import util.go_utils as go_utils
14
15 import stream_index
16 import banners
17 import playcontrol
18 import presenter
19
20
21 _moduleLogger = logging.getLogger(__name__)
22
23
24 class BasicWindow(gobject.GObject, go_utils.AutoSignal):
25
26         __gsignals__ = {
27                 'quit' : (
28                         gobject.SIGNAL_RUN_LAST,
29                         gobject.TYPE_NONE,
30                         (),
31                 ),
32                 'home' : (
33                         gobject.SIGNAL_RUN_LAST,
34                         gobject.TYPE_NONE,
35                         (),
36                 ),
37                 'jump-to' : (
38                         gobject.SIGNAL_RUN_LAST,
39                         gobject.TYPE_NONE,
40                         (gobject.TYPE_PYOBJECT, ),
41                 ),
42                 'rotate' : (
43                         gobject.SIGNAL_RUN_LAST,
44                         gobject.TYPE_NONE,
45                         (gobject.TYPE_BOOLEAN, ),
46                 ),
47                 'fullscreen' : (
48                         gobject.SIGNAL_RUN_LAST,
49                         gobject.TYPE_NONE,
50                         (gobject.TYPE_BOOLEAN, ),
51                 ),
52         }
53
54         def __init__(self, player, store):
55                 gobject.GObject.__init__(self)
56                 self._isDestroyed = False
57
58                 self._player = player
59                 self._store = store
60
61                 self._clipboard = gtk.clipboard_get()
62                 self._windowInFullscreen = False
63
64                 self._errorBanner = banners.StackingBanner()
65
66                 self._layout = gtk.VBox()
67                 self._layout.pack_start(self._errorBanner.toplevel, False, True)
68
69                 self._window = gtk.Window()
70                 go_utils.AutoSignal.__init__(self, self.window)
71                 self._window.add(self._layout)
72                 self._window = hildonize.hildonize_window(self, self._window)
73
74                 self._window.set_icon(self._store.get_pixbuf_from_store(self._store.STORE_LOOKUP["icon"]))
75                 self._window.connect("key-press-event", self._on_key_press)
76                 self._window.connect("window-state-event", self._on_window_state_change)
77                 self._window.connect("destroy", self._on_destroy)
78
79         @property
80         def window(self):
81                 return self._window
82
83         def show(self):
84                 self._window.show_all()
85
86         def save_settings(self, config, sectionName):
87                 config.add_section(sectionName)
88                 config.set(sectionName, "fullscreen", str(self._windowInFullscreen))
89
90         def load_settings(self, config, sectionName):
91                 try:
92                         self._windowInFullscreen = config.getboolean(sectionName, "fullscreen")
93                 except ConfigParser.NoSectionError, e:
94                         _moduleLogger.info(
95                                 "Settings file %s is missing section %s" % (
96                                         constants._user_settings_,
97                                         e.section,
98                                 )
99                         )
100
101                 if self._windowInFullscreen:
102                         self._window.fullscreen()
103                 else:
104                         self._window.unfullscreen()
105
106         def jump_to(self, node):
107                 raise NotImplementedError("On %s" % self)
108
109         @misc_utils.log_exception(_moduleLogger)
110         def _on_destroy(self, *args):
111                 self._isDestroyed = True
112
113         @misc_utils.log_exception(_moduleLogger)
114         def _on_window_state_change(self, widget, event, *args):
115                 if event.new_window_state & gtk.gdk.WINDOW_STATE_FULLSCREEN:
116                         self._windowInFullscreen = True
117                 else:
118                         self._windowInFullscreen = False
119                 self.emit("fullscreen", self._windowInFullscreen)
120
121         @misc_utils.log_exception(_moduleLogger)
122         def _on_key_press(self, widget, event, *args):
123                 RETURN_TYPES = (gtk.keysyms.Return, gtk.keysyms.ISO_Enter, gtk.keysyms.KP_Enter)
124                 isCtrl = bool(event.get_state() & gtk.gdk.CONTROL_MASK)
125                 if (
126                         event.keyval == gtk.keysyms.F6 or
127                         event.keyval in RETURN_TYPES and isCtrl
128                 ):
129                         # The "Full screen" hardware key has been pressed
130                         if self._windowInFullscreen:
131                                 self._window.unfullscreen ()
132                         else:
133                                 self._window.fullscreen ()
134                         return True
135                 elif (
136                         event.keyval in (gtk.keysyms.w, ) and
137                         event.get_state() & gtk.gdk.CONTROL_MASK
138                 ):
139                         self._window.destroy()
140                 elif (
141                         event.keyval in (gtk.keysyms.q, ) and
142                         event.get_state() & gtk.gdk.CONTROL_MASK
143                 ):
144                         self.emit("quit")
145                 elif event.keyval == gtk.keysyms.l and event.get_state() & gtk.gdk.CONTROL_MASK:
146                         with open(constants._user_logpath_, "r") as f:
147                                 logLines = f.xreadlines()
148                                 log = "".join(logLines)
149                                 self._clipboard.set_text(str(log))
150                         return True
151
152         @misc_utils.log_exception(_moduleLogger)
153         def _on_home(self, *args):
154                 self.emit("home")
155                 self._window.destroy()
156
157         @misc_utils.log_exception(_moduleLogger)
158         def _on_jump(self, source, node):
159                 raise NotImplementedError("On %s" % self)
160
161         @misc_utils.log_exception(_moduleLogger)
162         def _on_quit(self, *args):
163                 self.emit("quit")
164                 self._window.destroy()
165
166
167 class SourceSelector(BasicWindow):
168
169         def __init__(self, player, store, index):
170                 BasicWindow.__init__(self, player, store)
171                 self._languages = []
172                 self._index = index
173
174                 self._loadingBanner = banners.GenericBanner()
175
176                 self._radioButton = self._create_button("radio", "Radio")
177                 self._radioButton.connect("clicked", self._on_source_selected, stream_index.SOURCE_RADIO)
178                 self._radioWrapper = gtk.VBox()
179                 self._radioWrapper.pack_start(self._radioButton, False, True)
180
181                 self._conferenceButton = self._create_button("conferences", "Conferences")
182                 self._conferenceButton.connect("clicked", self._on_source_selected, stream_index.SOURCE_CONFERENCES)
183                 self._conferenceWrapper = gtk.VBox()
184                 self._conferenceWrapper.pack_start(self._conferenceButton, False, True)
185
186                 self._magazineButton = self._create_button("magazines", "Magazines")
187                 self._magazineButton.connect("clicked", self._on_source_selected, stream_index.SOURCE_MAGAZINES)
188                 self._magazineWrapper = gtk.VBox()
189                 self._magazineWrapper.pack_start(self._magazineButton, False, True)
190
191                 self._scriptureButton = self._create_button("scriptures", "Scriptures")
192                 self._scriptureButton.connect("clicked", self._on_source_selected, stream_index.SOURCE_SCRIPTURES)
193                 self._scriptureWrapper = gtk.VBox()
194                 self._scriptureWrapper.pack_start(self._scriptureButton, False, True)
195
196                 self._buttonLayout = gtk.VButtonBox()
197                 self._buttonLayout.set_layout(gtk.BUTTONBOX_SPREAD)
198                 self._buttonLayout.pack_start(self._radioWrapper, True, True)
199                 self._buttonLayout.pack_start(self._conferenceWrapper, True, True)
200                 self._buttonLayout.pack_start(self._magazineWrapper, True, True)
201                 self._buttonLayout.pack_start(self._scriptureWrapper, True, True)
202
203                 self._playcontrol = playcontrol.NavControl(player, store)
204                 self._playcontrol.connect("jump-to", self._on_jump)
205
206                 self._layout.pack_start(self._loadingBanner.toplevel, False, False)
207                 self._layout.pack_start(self._buttonLayout, True, True)
208                 self._layout.pack_start(self._playcontrol.toplevel, False, True)
209
210                 self._window.set_title(constants.__pretty_app_name__)
211
212         def show(self):
213                 BasicWindow.show(self)
214
215                 self._errorBanner.toplevel.hide()
216                 self._playcontrol.toplevel.hide()
217
218                 self._refresh()
219
220         def _show_loading(self):
221                 animationPath = self._store.STORE_LOOKUP["loading"]
222                 animation = self._store.get_pixbuf_animation_from_store(animationPath)
223                 self._loadingBanner.show(animation, "Loading...")
224                 self._buttonLayout.set_sensitive(False)
225
226         def _hide_loading(self):
227                 self._loadingBanner.hide()
228                 self._buttonLayout.set_sensitive(True)
229
230         def _refresh(self):
231                 self._show_loading()
232                 self._index.get_languages(self._on_languages, self._on_error)
233
234         def _create_button(self, icon, message):
235                 image = self._store.get_image_from_store(self._store.STORE_LOOKUP[icon])
236
237                 label = gtk.Label()
238                 label.set_text(message)
239
240                 buttonLayout = gtk.HBox(False, 5)
241                 buttonLayout.pack_start(image, False, False)
242                 buttonLayout.pack_start(label, False, True)
243                 button = gtk.Button()
244                 button.add(buttonLayout)
245
246                 return button
247
248         @misc_utils.log_exception(_moduleLogger)
249         def _on_languages(self, languages):
250                 self._hide_loading()
251                 self._languages = list(languages)
252
253         @misc_utils.log_exception(_moduleLogger)
254         def _on_error(self, exception):
255                 self._hide_loading()
256                 self._errorBanner.push_message(str(exception))
257
258         def _window_from_node(self, node):
259                 if node.id == stream_index.SOURCE_RADIO:
260                         Source = RadioWindow
261                 elif node.id == stream_index.SOURCE_CONFERENCES:
262                         Source = ConferencesWindow
263                 elif node.id == stream_index.SOURCE_MAGAZINES:
264                         pass
265                 elif node.id == stream_index.SOURCE_SCRIPTURES:
266                         pass
267                 sourceWindow = Source(self._player, self._store, node)
268                 sourceWindow.window.set_modal(True)
269                 sourceWindow.window.set_transient_for(self._window)
270                 sourceWindow.window.set_default_size(*self._window.get_size())
271                 sourceWindow.connect("quit", self._on_quit)
272                 sourceWindow.connect("jump-to", self._on_jump)
273                 sourceWindow.show()
274                 return sourceWindow
275
276         @misc_utils.log_exception(_moduleLogger)
277         def _on_jump(self, source, node):
278                 targetNodePath = list(reversed(list(stream_index.walk_ancestors(node))))
279                 ancestor = targetNodePath[0]
280                 window = self._window_from_node(ancestor)
281                 window.jump_to(node)
282
283         @misc_utils.log_exception(_moduleLogger)
284         def _on_source_selected(self, widget, nodeName):
285                 node = self._index.get_source(nodeName, self._languages[0]["id"])
286                 self._window_from_node(node)
287
288
289 gobject.type_register(SourceSelector)
290
291
292 class RadioWindow(BasicWindow):
293
294         def __init__(self, player, store, node):
295                 BasicWindow.__init__(self, player, store)
296                 self._node = node
297                 self._childNode = None
298
299                 self.connect_auto(self._player, "state-change", self._on_player_state_change)
300                 self.connect_auto(self._player, "title-change", self._on_player_title_change)
301
302                 self._loadingBanner = banners.GenericBanner()
303
304                 headerPath = self._store.STORE_LOOKUP["radio_header"]
305                 self._header = self._store.get_image_from_store(headerPath)
306                 self._headerNavigation = presenter.NavigationBox()
307                 self._headerNavigation.toplevel.add(self._header)
308                 self._headerNavigation.connect("action", self._on_nav_action)
309                 self._headerNavigation.connect("navigating", self._on_navigating)
310
311                 self._programmingModel = gtk.ListStore(
312                         gobject.TYPE_STRING,
313                         gobject.TYPE_STRING,
314                 )
315
316                 textrenderer = gtk.CellRendererText()
317                 timeColumn = gtk.TreeViewColumn("Time")
318                 timeColumn.pack_start(textrenderer, expand=True)
319                 timeColumn.add_attribute(textrenderer, "text", 0)
320
321                 textrenderer = gtk.CellRendererText()
322                 titleColumn = gtk.TreeViewColumn("Program")
323                 titleColumn.pack_start(textrenderer, expand=True)
324                 titleColumn.add_attribute(textrenderer, "text", 1)
325
326                 self._treeView = gtk.TreeView()
327                 self._treeView.set_headers_visible(False)
328                 self._treeView.set_model(self._programmingModel)
329                 self._treeView.append_column(timeColumn)
330                 self._treeView.append_column(titleColumn)
331                 self._treeView.get_selection().connect("changed", self._on_row_changed)
332
333                 self._treeScroller = gtk.ScrolledWindow()
334                 self._treeScroller.add(self._treeView)
335                 self._treeScroller.set_policy(gtk.POLICY_NEVER, gtk.POLICY_AUTOMATIC)
336
337                 self._presenter = presenter.StreamMiniPresenter(self._store)
338                 self._presenterNavigation = presenter.NavigationBox()
339                 self._presenterNavigation.toplevel.add(self._presenter.toplevel)
340                 self._presenterNavigation.connect("action", self._on_nav_action)
341                 self._presenterNavigation.connect("navigating", self._on_navigating)
342
343                 self._radioLayout = gtk.VBox(False)
344                 self._radioLayout.pack_start(self._headerNavigation.toplevel, False, False)
345                 self._radioLayout.pack_start(self._treeScroller, True, True)
346                 self._radioLayout.pack_start(self._presenterNavigation.toplevel, False, True)
347
348                 self._layout.pack_start(self._loadingBanner.toplevel, False, False)
349                 self._layout.pack_start(self._radioLayout, True, True)
350
351                 self._window.set_title(self._node.title)
352                 self._dateShown = datetime.datetime.now()
353
354         def show(self):
355                 BasicWindow.show(self)
356
357                 self._errorBanner.toplevel.hide()
358                 self._loadingBanner.toplevel.hide()
359
360                 self._refresh()
361
362         def jump_to(self, node):
363                 _moduleLogger.info("Only 1 channel, nothing to jump to")
364
365         @property
366         def _active(self):
367                 return self._player.node is self._childNode
368
369         def _set_context(self, state):
370                 if state == self._player.STATE_PLAY:
371                         if self._active:
372                                 self._presenter.set_state(self._store.STORE_LOOKUP["pause"])
373                         else:
374                                 self._presenter.set_state(self._store.STORE_LOOKUP["play"])
375                 elif state == self._player.STATE_PAUSE:
376                         self._presenter.set_state(self._store.STORE_LOOKUP["play"])
377                 elif state == self._player.STATE_STOP:
378                         self._presenter.set_state(self._store.STORE_LOOKUP["play"])
379                 else:
380                         _moduleLogger.info("Unhandled player state %s" % state)
381                         self._presenter.set_state(self._store.STORE_LOOKUP["play"])
382
383         def _show_loading(self):
384                 animationPath = self._store.STORE_LOOKUP["loading"]
385                 animation = self._store.get_pixbuf_animation_from_store(animationPath)
386                 self._loadingBanner.show(animation, "Loading...")
387
388         def _hide_loading(self):
389                 self._loadingBanner.hide()
390
391         def _refresh(self):
392                 self._show_loading()
393                 self._programmingModel.clear()
394                 self._node.get_children(
395                         self._on_channels,
396                         self._on_load_error,
397                 )
398                 self._set_context(self._player.state)
399
400         def _get_current_row(self):
401                 nowTime = self._dateShown.strftime("%H:%M:%S")
402                 i = 0
403                 for i, row in enumerate(self._programmingModel):
404                         if nowTime < row[0]:
405                                 if i == 0:
406                                         return 0
407                                 else:
408                                         return i - 1
409                 else:
410                         return i
411
412         @misc_utils.log_exception(_moduleLogger)
413         def _on_player_state_change(self, player, newState):
414                 if self._headerNavigation.is_active() or self._presenterNavigation.is_active():
415                         return
416
417                 self._set_context(newState)
418
419         @misc_utils.log_exception(_moduleLogger)
420         def _on_player_title_change(self, player, node):
421                 if node is not self._childNode or node is None:
422                         _moduleLogger.info("Player title magically changed to %s" % player.title)
423                         return
424
425         @misc_utils.log_exception(_moduleLogger)
426         def _on_navigating(self, widget, navState):
427                 if navState == "clicking":
428                         if self._player.state == self._player.STATE_PLAY:
429                                 if self._active:
430                                         imageName = "pause"
431                                 else:
432                                         imageName = "play"
433                         elif self._player.state == self._player.STATE_PAUSE:
434                                 imageName = "play"
435                         elif self._player.state == self._player.STATE_STOP:
436                                 imageName = "play"
437                         else:
438                                 imageName = "play"
439                                 _moduleLogger.info("Unhandled player state %s" % self._player.state)
440                 elif navState == "down":
441                         imageName = "home"
442                 elif navState == "up":
443                         imageName = "play"
444                 elif navState == "left":
445                         imageName = "play"
446                 elif navState == "right":
447                         imageName = "play"
448
449                 self._presenter.set_state(self._store.STORE_LOOKUP[imageName])
450
451         @misc_utils.log_exception(_moduleLogger)
452         def _on_nav_action(self, widget, navState):
453                 self._set_context(self._player.state)
454
455                 if navState == "clicking":
456                         if self._player.state == self._player.STATE_PLAY:
457                                 if self._active:
458                                         self._player.pause()
459                                 else:
460                                         self._player.set_piece_by_node(self._childNode)
461                                         self._player.play()
462                         elif self._player.state == self._player.STATE_PAUSE:
463                                 self._player.play()
464                         elif self._player.state == self._player.STATE_STOP:
465                                 self._player.set_piece_by_node(self._childNode)
466                                 self._player.play()
467                         else:
468                                 _moduleLogger.info("Unhandled player state %s" % self._player.state)
469                 elif navState == "down":
470                         self.window.destroy()
471                 elif navState == "up":
472                         pass
473                 elif navState == "left":
474                         self._dateShown += datetime.timedelta(days=1)
475                         self._refresh()
476                 elif navState == "right":
477                         self._dateShown -= datetime.timedelta(days=1)
478                         self._refresh()
479
480         @misc_utils.log_exception(_moduleLogger)
481         def _on_channels(self, channels):
482                 if self._isDestroyed:
483                         _moduleLogger.info("Download complete but window destroyed")
484                         return
485
486                 channels = channels
487                 if 1 < len(channels):
488                         _moduleLogger.warning("More channels now available!")
489                 self._childNode = channels[0]
490                 self._childNode.get_programming(
491                         self._dateShown,
492                         self._on_channel,
493                         self._on_load_error,
494                 )
495
496         @misc_utils.log_exception(_moduleLogger)
497         def _on_channel(self, programs):
498                 if self._isDestroyed:
499                         _moduleLogger.info("Download complete but window destroyed")
500                         return
501
502                 self._hide_loading()
503                 for program in programs:
504                         row = program["time"], program["title"]
505                         self._programmingModel.append(row)
506
507                 currentDate = datetime.datetime.now()
508                 if currentDate.date() != self._dateShown.date():
509                         self._treeView.get_selection().set_mode(gtk.SELECTION_NONE)
510                 else:
511                         self._treeView.get_selection().set_mode(gtk.SELECTION_SINGLE)
512                         path = (self._get_current_row(), )
513                         self._treeView.scroll_to_cell(path)
514                         self._treeView.get_selection().select_path(path)
515
516         @misc_utils.log_exception(_moduleLogger)
517         def _on_load_error(self, exception):
518                 self._hide_loading()
519                 self._errorBanner.push_message(str(exception))
520
521         @misc_utils.log_exception(_moduleLogger)
522         def _on_row_changed(self, selection):
523                 if len(self._programmingModel) == 0:
524                         return
525
526                 rowIndex = self._get_current_row()
527                 path = (rowIndex, )
528                 if not selection.path_is_selected(path):
529                         # Undo the user's changing of the selection
530                         selection.select_path(path)
531
532
533 gobject.type_register(RadioWindow)
534
535
536 class ListWindow(BasicWindow):
537
538         def __init__(self, player, store, node):
539                 BasicWindow.__init__(self, player, store)
540                 self._node = node
541
542                 self._loadingBanner = banners.GenericBanner()
543
544                 modelTypes, columns = zip(*self._get_columns())
545
546                 self._model = gtk.ListStore(*modelTypes)
547
548                 self._treeView = gtk.TreeView()
549                 self._treeView.connect("row-activated", self._on_row_activated)
550                 self._treeView.set_headers_visible(False)
551                 self._treeView.set_model(self._model)
552                 for column in columns:
553                         if column is not None:
554                                 self._treeView.append_column(column)
555
556                 self._treeScroller = gtk.ScrolledWindow()
557                 self._treeScroller.add(self._treeView)
558                 self._treeScroller.set_policy(gtk.POLICY_NEVER, gtk.POLICY_AUTOMATIC)
559
560                 self._separator = gtk.HSeparator()
561                 self._playcontrol = playcontrol.NavControl(self._player, self._store)
562                 self._playcontrol.connect("home", self._on_home)
563                 self._playcontrol.connect("jump-to", self._on_jump)
564
565                 self._contentLayout = gtk.VBox(False)
566                 self._contentLayout.pack_start(self._treeScroller, True, True)
567                 self._contentLayout.pack_start(self._separator, False, True)
568                 self._contentLayout.pack_start(self._playcontrol.toplevel, False, True)
569
570                 self._layout.pack_start(self._loadingBanner.toplevel, False, False)
571                 self._layout.pack_start(self._contentLayout, True, True)
572
573         def show(self):
574                 BasicWindow.show(self)
575
576                 self._errorBanner.toplevel.hide()
577                 self._loadingBanner.toplevel.hide()
578
579                 self._refresh()
580                 self._playcontrol.refresh()
581
582         @classmethod
583         def _get_columns(cls):
584                 raise NotImplementedError("")
585
586         def _get_current_row(self):
587                 if self._player.node is None:
588                         return -1
589                 ancestors, current, descendants = stream_index.common_paths(self._player.node, self._node)
590                 if not descendants:
591                         return -1
592                 activeChild = descendants[0]
593                 for i, row in enumerate(self._model):
594                         if activeChild is row[0]:
595                                 return i
596                 else:
597                         return -1
598
599         def jump_to(self, node):
600                 ancestors, current, descendants = stream_index.common_paths(node, self._node)
601                 if current is None:
602                         raise RuntimeError("Cannot jump to node %s" % node)
603                 if not descendants:
604                         _moduleLogger.info("Current node is the target")
605                         return
606                 child = descendants[0]
607                 window = self._window_from_node(child)
608                 window.jump_to(node)
609
610         def _window_from_node(self, node):
611                 raise NotImplementedError("")
612
613         @misc_utils.log_exception(_moduleLogger)
614         def _on_row_activated(self, view, path, column):
615                 raise NotImplementedError("")
616
617         @misc_utils.log_exception(_moduleLogger)
618         def _on_jump(self, source, node):
619                 ancestors, current, descendants = stream_index.common_paths(node, self._node)
620                 if current is None:
621                         _moduleLogger.info("%s is not the target, moving up" % self._node)
622                         self.emit("jump-to", node)
623                         self._window.destroy()
624                         return
625                 if not descendants:
626                         _moduleLogger.info("Current node is the target")
627                         return
628                 child = descendants[0]
629                 window = self._window_from_node(child)
630                 window.jump_to(node)
631
632         def _show_loading(self):
633                 animationPath = self._store.STORE_LOOKUP["loading"]
634                 animation = self._store.get_pixbuf_animation_from_store(animationPath)
635                 self._loadingBanner.show(animation, "Loading...")
636
637         def _hide_loading(self):
638                 self._loadingBanner.hide()
639
640         def _refresh(self):
641                 self._show_loading()
642                 self._model.clear()
643
644         def _select_row(self):
645                 path = (self._get_current_row(), )
646                 self._treeView.scroll_to_cell(path)
647                 self._treeView.get_selection().select_path(path)
648
649
650 class ConferencesWindow(ListWindow):
651
652         def __init__(self, player, store, node):
653                 ListWindow.__init__(self, player, store, node)
654                 self._window.set_title(self._node.title)
655
656         @classmethod
657         def _get_columns(cls):
658                 yield gobject.TYPE_PYOBJECT, None
659
660                 textrenderer = gtk.CellRendererText()
661                 column = gtk.TreeViewColumn("Date")
662                 column.pack_start(textrenderer, expand=True)
663                 column.add_attribute(textrenderer, "text", 1)
664                 yield gobject.TYPE_STRING, column
665
666                 textrenderer = gtk.CellRendererText()
667                 column = gtk.TreeViewColumn("Conference")
668                 column.pack_start(textrenderer, expand=True)
669                 column.add_attribute(textrenderer, "text", 2)
670                 yield gobject.TYPE_STRING, column
671
672         def _refresh(self):
673                 ListWindow._refresh(self)
674                 self._node.get_children(
675                         self._on_conferences,
676                         self._on_error,
677                 )
678
679         @misc_utils.log_exception(_moduleLogger)
680         def _on_conferences(self, programs):
681                 if self._isDestroyed:
682                         _moduleLogger.info("Download complete but window destroyed")
683                         return
684
685                 self._hide_loading()
686                 for programNode in programs:
687                         program = programNode.get_properties()
688                         row = programNode, program["title"], program["full_title"]
689                         self._model.append(row)
690
691                 path = (self._get_current_row(), )
692                 self._treeView.scroll_to_cell(path)
693                 self._treeView.get_selection().select_path(path)
694
695         @misc_utils.log_exception(_moduleLogger)
696         def _on_error(self, exception):
697                 self._hide_loading()
698                 self._errorBanner.push_message(str(exception))
699
700         def _window_from_node(self, node):
701                 sessionsWindow = ConferenceSessionsWindow(self._player, self._store, node)
702                 sessionsWindow.window.set_modal(True)
703                 sessionsWindow.window.set_transient_for(self._window)
704                 sessionsWindow.window.set_default_size(*self._window.get_size())
705                 sessionsWindow.connect("quit", self._on_quit)
706                 sessionsWindow.connect("home", self._on_home)
707                 sessionsWindow.connect("jump-to", self._on_jump)
708                 sessionsWindow.show()
709                 return sessionsWindow
710
711         @misc_utils.log_exception(_moduleLogger)
712         def _on_row_activated(self, view, path, column):
713                 itr = self._model.get_iter(path)
714                 node = self._model.get_value(itr, 0)
715                 self._window_from_node(node)
716
717
718 gobject.type_register(ConferencesWindow)
719
720
721 class ConferenceSessionsWindow(ListWindow):
722
723         def __init__(self, player, store, node):
724                 ListWindow.__init__(self, player, store, node)
725                 self._window.set_title(self._node.title)
726
727         @classmethod
728         def _get_columns(cls):
729                 yield gobject.TYPE_PYOBJECT, None
730
731                 textrenderer = gtk.CellRendererText()
732                 column = gtk.TreeViewColumn("Session")
733                 column.pack_start(textrenderer, expand=True)
734                 column.add_attribute(textrenderer, "text", 1)
735                 yield gobject.TYPE_STRING, column
736
737         def _refresh(self):
738                 ListWindow._refresh(self)
739                 self._node.get_children(
740                         self._on_conference_sessions,
741                         self._on_error,
742                 )
743
744         @misc_utils.log_exception(_moduleLogger)
745         def _on_conference_sessions(self, programs):
746                 if self._isDestroyed:
747                         _moduleLogger.info("Download complete but window destroyed")
748                         return
749
750                 self._hide_loading()
751                 for programNode in programs:
752                         program = programNode.get_properties()
753                         row = programNode, program["title"]
754                         self._model.append(row)
755
756                 path = (self._get_current_row(), )
757                 self._treeView.scroll_to_cell(path)
758                 self._treeView.get_selection().select_path(path)
759
760         @misc_utils.log_exception(_moduleLogger)
761         def _on_error(self, exception):
762                 self._hide_loading()
763                 self._errorBanner.push_message(str(exception))
764
765         def _window_from_node(self, node):
766                 sessionsWindow = ConferenceTalksWindow(self._player, self._store, node)
767                 sessionsWindow.window.set_modal(True)
768                 sessionsWindow.window.set_transient_for(self._window)
769                 sessionsWindow.window.set_default_size(*self._window.get_size())
770                 sessionsWindow.connect("quit", self._on_quit)
771                 sessionsWindow.connect("home", self._on_home)
772                 sessionsWindow.connect("jump-to", self._on_jump)
773                 sessionsWindow.show()
774                 return sessionsWindow
775
776         @misc_utils.log_exception(_moduleLogger)
777         def _on_row_activated(self, view, path, column):
778                 itr = self._model.get_iter(path)
779                 node = self._model.get_value(itr, 0)
780                 self._window_from_node(node)
781
782
783 gobject.type_register(ConferenceSessionsWindow)
784
785
786 class ConferenceTalksWindow(ListWindow):
787
788         def __init__(self, player, store, node):
789                 ListWindow.__init__(self, player, store, node)
790                 self._window.set_title(self._node.title)
791
792         @classmethod
793         def _get_columns(cls):
794                 yield gobject.TYPE_PYOBJECT, None
795
796                 textrenderer = gtk.CellRendererText()
797                 column = gtk.TreeViewColumn("Talk")
798                 column.pack_start(textrenderer, expand=True)
799                 column.add_attribute(textrenderer, "text", 1)
800                 yield gobject.TYPE_STRING, column
801
802         def _refresh(self):
803                 ListWindow._refresh(self)
804                 self._node.get_children(
805                         self._on_conference_talks,
806                         self._on_error,
807                 )
808
809         @misc_utils.log_exception(_moduleLogger)
810         def _on_conference_talks(self, programs):
811                 if self._isDestroyed:
812                         _moduleLogger.info("Download complete but window destroyed")
813                         return
814
815                 self._hide_loading()
816                 for programNode in programs:
817                         program = programNode.get_properties()
818                         row = programNode, "%s\n%s" % (program["title"], program["speaker"])
819                         self._model.append(row)
820
821                 path = (self._get_current_row(), )
822                 self._treeView.scroll_to_cell(path)
823                 self._treeView.get_selection().select_path(path)
824
825         @misc_utils.log_exception(_moduleLogger)
826         def _on_error(self, exception):
827                 self._hide_loading()
828                 self._errorBanner.push_message(str(exception))
829
830         def _window_from_node(self, node):
831                 sessionsWindow = ConferenceTalkWindow(self._player, self._store, node)
832                 sessionsWindow.window.set_modal(True)
833                 sessionsWindow.window.set_transient_for(self._window)
834                 sessionsWindow.window.set_default_size(*self._window.get_size())
835                 sessionsWindow.connect("quit", self._on_quit)
836                 sessionsWindow.connect("home", self._on_home)
837                 sessionsWindow.connect("jump-to", self._on_jump)
838                 sessionsWindow.show()
839                 return sessionsWindow
840
841         @misc_utils.log_exception(_moduleLogger)
842         def _on_row_activated(self, view, path, column):
843                 itr = self._model.get_iter(path)
844                 node = self._model.get_value(itr, 0)
845                 self._window_from_node(node)
846
847
848 gobject.type_register(ConferenceTalksWindow)
849
850
851 class ConferenceTalkWindow(BasicWindow):
852
853         def __init__(self, player, store, node):
854                 BasicWindow.__init__(self, player, store)
855                 self._node = node
856
857                 self.connect_auto(self._player, "state-change", self._on_player_state_change)
858                 self.connect_auto(self._player, "title-change", self._on_player_title_change)
859                 self.connect_auto(self._player, "error", self._on_player_error)
860
861                 self._loadingBanner = banners.GenericBanner()
862
863                 self._presenter = presenter.StreamPresenter(self._store)
864                 self._presenterNavigation = presenter.NavigationBox()
865                 self._presenterNavigation.toplevel.add(self._presenter.toplevel)
866                 self._presenterNavigation.connect("action", self._on_nav_action)
867                 self._presenterNavigation.connect("navigating", self._on_navigating)
868
869                 self._layout.pack_start(self._loadingBanner.toplevel, False, False)
870                 self._layout.pack_start(self._presenterNavigation.toplevel, True, True)
871
872                 self._window.set_title(self._node.title)
873
874         def show(self):
875                 BasicWindow.show(self)
876                 self._window.show_all()
877                 self._errorBanner.toplevel.hide()
878                 self._loadingBanner.toplevel.hide()
879
880                 self._presenter.set_context(
881                         self._store.STORE_LOOKUP["conference_background"],
882                         self._player.title,
883                         self._player.subtitle,
884                 )
885                 self._set_context(self._player.state)
886
887         def jump_to(self, node):
888                 assert self._node is node
889
890         @property
891         def _active(self):
892                 return self._player.node is self._node
893
894         def _show_loading(self):
895                 animationPath = self._store.STORE_LOOKUP["loading"]
896                 animation = self._store.get_pixbuf_animation_from_store(animationPath)
897                 self._loadingBanner.show(animation, "Loading...")
898
899         def _hide_loading(self):
900                 self._loadingBanner.hide()
901
902         def _set_context(self, state):
903                 if state == self._player.STATE_PLAY:
904                         if self._active:
905                                 self._presenter.set_state(self._store.STORE_LOOKUP["pause"])
906                         else:
907                                 self._presenter.set_state(self._store.STORE_LOOKUP["play"])
908                 elif state == self._player.STATE_PAUSE:
909                         self._presenter.set_state(self._store.STORE_LOOKUP["play"])
910                 elif state == self._player.STATE_STOP:
911                         self._presenter.set_state(self._store.STORE_LOOKUP["play"])
912                 else:
913                         _moduleLogger.info("Unhandled player state %s" % state)
914
915         @misc_utils.log_exception(_moduleLogger)
916         def _on_player_state_change(self, player, newState):
917                 if self._presenterNavigation.is_active():
918                         return
919
920                 self._set_context(newState)
921
922         @misc_utils.log_exception(_moduleLogger)
923         def _on_player_title_change(self, player, node):
924                 if node is not self._node or node is None:
925                         _moduleLogger.info("Player title magically changed to %s" % player.title)
926                         return
927                 self._presenter.set_context(
928                         self._store.STORE_LOOKUP["conference_background"],
929                         self._player.title,
930                         self._player.subtitle,
931                 )
932
933         @misc_utils.log_exception(_moduleLogger)
934         def _on_player_error(self, player, err, debug):
935                 _moduleLogger.error("%r - %r" % (err, debug))
936
937         @misc_utils.log_exception(_moduleLogger)
938         def _on_navigating(self, widget, navState):
939                 if navState == "clicking":
940                         if self._player.state == self._player.STATE_PLAY:
941                                 if self._active:
942                                         imageName = "pause"
943                                 else:
944                                         imageName = "play"
945                         elif self._player.state == self._player.STATE_PAUSE:
946                                 imageName = "play"
947                         elif self._player.state == self._player.STATE_STOP:
948                                 imageName = "play"
949                         else:
950                                 _moduleLogger.info("Unhandled player state %s" % self._player.state)
951                 elif navState == "down":
952                         imageName = "home"
953                 elif navState == "up":
954                         imageName = "play"
955                 elif navState == "left":
956                         imageName = "next"
957                 elif navState == "right":
958                         imageName = "prev"
959
960                 self._presenter.set_state(self._store.STORE_LOOKUP[imageName])
961
962         @misc_utils.log_exception(_moduleLogger)
963         def _on_nav_action(self, widget, navState):
964                 self._set_context(self._player.state)
965
966                 if navState == "clicking":
967                         if self._player.state == self._player.STATE_PLAY:
968                                 if self._active:
969                                         self._player.pause()
970                                 else:
971                                         self._player.set_piece_by_node(self._node)
972                                         self._player.play()
973                         elif self._player.state == self._player.STATE_PAUSE:
974                                 self._player.play()
975                         elif self._player.state == self._player.STATE_STOP:
976                                 self._player.set_piece_by_node(self._node)
977                                 self._player.play()
978                         else:
979                                 _moduleLogger.info("Unhandled player state %s" % self._player.state)
980                 elif navState == "down":
981                         self.emit("home")
982                         self._window.destroy()
983                 elif navState == "up":
984                         pass
985                 elif navState == "left":
986                         self._player.next()
987                 elif navState == "right":
988                         self._player.back()
989
990
991 gobject.type_register(ConferenceTalkWindow)