Bump to 1.0.6
[watersofshiloah] / src / windows / _base.py
1 from __future__ import with_statement
2
3 import ConfigParser
4 import logging
5 import webbrowser
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 presenter
18
19
20 _moduleLogger = logging.getLogger(__name__)
21
22
23 class BasicWindow(gobject.GObject, go_utils.AutoSignal):
24
25         __gsignals__ = {
26                 'quit' : (
27                         gobject.SIGNAL_RUN_LAST,
28                         gobject.TYPE_NONE,
29                         (),
30                 ),
31                 'home' : (
32                         gobject.SIGNAL_RUN_LAST,
33                         gobject.TYPE_NONE,
34                         (),
35                 ),
36                 'jump-to' : (
37                         gobject.SIGNAL_RUN_LAST,
38                         gobject.TYPE_NONE,
39                         (gobject.TYPE_PYOBJECT, ),
40                 ),
41                 'rotate' : (
42                         gobject.SIGNAL_RUN_LAST,
43                         gobject.TYPE_NONE,
44                         (gobject.TYPE_BOOLEAN, ),
45                 ),
46                 'fullscreen' : (
47                         gobject.SIGNAL_RUN_LAST,
48                         gobject.TYPE_NONE,
49                         (gobject.TYPE_BOOLEAN, ),
50                 ),
51                 'rotate' : (
52                         gobject.SIGNAL_RUN_LAST,
53                         gobject.TYPE_NONE,
54                         (gobject.TYPE_PYOBJECT, ),
55                 ),
56         }
57
58         def __init__(self, app, player, store):
59                 gobject.GObject.__init__(self)
60                 self._isDestroyed = False
61                 self._isPortrait = hildonize.IS_FREMANTLE_SUPPORTED
62
63                 self._app = app
64                 self._player = player
65                 self._store = store
66
67                 self._clipboard = gtk.clipboard_get()
68                 self._windowInFullscreen = False
69
70                 self._errorBanner = banners.StackingBanner()
71
72                 self._layout = gtk.VBox()
73
74                 self._window = gtk.Window()
75                 self._window.add(self._layout)
76                 self._window = hildonize.hildonize_window(self._app, self._window)
77                 go_utils.AutoSignal.__init__(self, self.window)
78
79                 self._window.set_icon(self._store.get_pixbuf_from_store(self._store.STORE_LOOKUP["icon"]))
80                 self._window.connect("key-press-event", self._on_key_press)
81                 self._window.connect("window-state-event", self._on_window_state_change)
82                 self._window.connect("destroy", self._on_destroy)
83
84                 if hildonize.GTK_MENU_USED:
85                         aboutMenuItem = gtk.MenuItem("About")
86                         aboutMenuItem.connect("activate", self._on_about)
87
88                         helpMenu = gtk.Menu()
89                         helpMenu.append(aboutMenuItem)
90
91                         helpMenuItem = gtk.MenuItem("Help")
92                         helpMenuItem.set_submenu(helpMenu)
93
94                         menuBar = gtk.MenuBar()
95                         menuBar.append(helpMenuItem)
96
97                         self._layout.pack_start(menuBar, False, False)
98                         menuBar = hildonize.hildonize_menu(
99                                 self._window,
100                                 menuBar,
101                         )
102                 else:
103                         aboutMenuItem = gtk.Button("About")
104                         aboutMenuItem.connect("clicked", self._on_about)
105
106                         appMenu = hildonize.hildon.AppMenu()
107                         appMenu.append(aboutMenuItem)
108                         appMenu.show_all()
109                         self._window.set_app_menu(appMenu)
110
111                 self._layout.pack_start(self._errorBanner.toplevel, False, True)
112
113         @property
114         def window(self):
115                 return self._window
116
117         def show(self):
118                 if self._isPortrait:
119                         hildonize.window_to_portrait(self._window)
120                 else:
121                         hildonize.window_to_landscape(self._window)
122                 self._window.show_all()
123
124         def save_settings(self, config, sectionName):
125                 config.add_section(sectionName)
126                 config.set(sectionName, "fullscreen", str(self._windowInFullscreen))
127
128         def load_settings(self, config, sectionName):
129                 try:
130                         windowInFullscreen = config.getboolean(sectionName, "fullscreen")
131                 except ConfigParser.NoSectionError, e:
132                         _moduleLogger.info(
133                                 "Settings file %s is missing section %s" % (
134                                         constants._user_settings_,
135                                         e.section,
136                                 )
137                         )
138                         windowInFullscreen = self._windowInFullscreen
139
140                 if windowInFullscreen:
141                         self._window.fullscreen()
142                 else:
143                         self._window.unfullscreen()
144
145         def jump_to(self, node):
146                 raise NotImplementedError("On %s" % self)
147
148         def set_orientation(self, orientation):
149                 oldIsPortrait = self._isPortrait
150                 if orientation == gtk.ORIENTATION_VERTICAL:
151                         hildonize.window_to_portrait(self._window)
152                         self._isPortrait = True
153                 elif orientation == gtk.ORIENTATION_HORIZONTAL:
154                         hildonize.window_to_landscape(self._window)
155                         self._isPortrait = False
156                 else:
157                         raise NotImplementedError(orientation)
158                 didChange = oldIsPortrait != self._isPortrait
159                 if didChange:
160                         self.emit("rotate", orientation)
161                 return didChange
162
163         def _configure_child(self, childWindow):
164                 if not hildonize.IS_FREMANTLE_SUPPORTED:
165                         childWindow.window.set_modal(True)
166                         childWindow.window.set_transient_for(self._window)
167                 childWindow.window.set_default_size(*self._window.get_size())
168                 if self._windowInFullscreen:
169                         childWindow.window.fullscreen()
170                 else:
171                         childWindow.window.unfullscreen()
172                 childWindow.set_orientation(
173                         gtk.ORIENTATION_VERTICAL if self._isPortrait else gtk.ORIENTATION_HORIZONTAL
174                 )
175                 childWindow.connect_auto(childWindow, "quit", self._on_quit)
176                 childWindow.connect_auto(childWindow, "home", self._on_home)
177                 childWindow.connect_auto(childWindow, "jump-to", self._on_jump)
178                 childWindow.connect_auto(childWindow, "fullscreen", self._on_child_fullscreen)
179                 childWindow.connect_auto(childWindow, "rotate", self._on_child_rotate)
180
181         @misc_utils.log_exception(_moduleLogger)
182         def _on_about(self, *args):
183                 sourceWindow = AboutWindow(self._app, self._player, self._store)
184                 self._configure_child(sourceWindow)
185                 sourceWindow.show()
186
187         @misc_utils.log_exception(_moduleLogger)
188         def _on_destroy(self, *args):
189                 self._isDestroyed = True
190
191         @misc_utils.log_exception(_moduleLogger)
192         def _on_window_state_change(self, widget, event, *args):
193                 oldIsFull = self._windowInFullscreen
194                 if event.new_window_state & gtk.gdk.WINDOW_STATE_FULLSCREEN:
195                         self._windowInFullscreen = True
196                 else:
197                         self._windowInFullscreen = False
198                 if oldIsFull != self._windowInFullscreen:
199                         _moduleLogger.info("%r Emit fullscreen %s" % (self, self._windowInFullscreen))
200                         self.emit("fullscreen", self._windowInFullscreen)
201
202         @misc_utils.log_exception(_moduleLogger)
203         def _on_key_press(self, widget, event, *args):
204                 RETURN_TYPES = (gtk.keysyms.Return, gtk.keysyms.ISO_Enter, gtk.keysyms.KP_Enter)
205                 isCtrl = bool(event.get_state() & gtk.gdk.CONTROL_MASK)
206                 if (
207                         event.keyval == gtk.keysyms.F6 or
208                         event.keyval in RETURN_TYPES and isCtrl
209                 ):
210                         # The "Full screen" hardware key has been pressed
211                         if self._windowInFullscreen:
212                                 self._window.unfullscreen ()
213                         else:
214                                 self._window.fullscreen ()
215                         return True
216                 elif event.keyval == gtk.keysyms.o and event.get_state() & gtk.gdk.CONTROL_MASK:
217                         if self._isPortrait:
218                                 self.set_orientation(gtk.ORIENTATION_HORIZONTAL)
219                         else:
220                                 self.set_orientation(gtk.ORIENTATION_VERTICAL)
221                         return True
222                 elif (
223                         event.keyval in (gtk.keysyms.w, ) and
224                         event.get_state() & gtk.gdk.CONTROL_MASK
225                 ):
226                         self._window.destroy()
227                 elif (
228                         event.keyval in (gtk.keysyms.q, ) and
229                         event.get_state() & gtk.gdk.CONTROL_MASK
230                 ):
231                         self.emit("quit")
232                         self._window.destroy()
233                 elif event.keyval == gtk.keysyms.l and event.get_state() & gtk.gdk.CONTROL_MASK:
234                         with open(constants._user_logpath_, "r") as f:
235                                 logLines = f.xreadlines()
236                                 log = "".join(logLines)
237                                 self._clipboard.set_text(str(log))
238                         return True
239
240         @misc_utils.log_exception(_moduleLogger)
241         def _on_home(self, *args):
242                 self.emit("home")
243                 self._window.destroy()
244
245         @misc_utils.log_exception(_moduleLogger)
246         def _on_child_fullscreen(self, source, isFull):
247                 if isFull:
248                         _moduleLogger.info("Full screen %r to mirror child %r" % (self, source))
249                         self._window.fullscreen()
250                 else:
251                         _moduleLogger.info("Unfull screen %r to mirror child %r" % (self, source))
252                         self._window.unfullscreen()
253
254         @misc_utils.log_exception(_moduleLogger)
255         def _on_child_rotate(self, source, orientation):
256                 self.set_orientation(orientation)
257
258         @misc_utils.log_exception(_moduleLogger)
259         def _on_jump(self, source, node):
260                 raise NotImplementedError("On %s" % self)
261
262         @misc_utils.log_exception(_moduleLogger)
263         def _on_quit(self, *args):
264                 self.emit("quit")
265                 self._window.destroy()
266
267
268 class ListWindow(BasicWindow):
269
270         def __init__(self, app, player, store, node):
271                 BasicWindow.__init__(self, app, player, store)
272                 self._node = node
273
274                 self.connect_auto(self._player, "title-change", self._on_player_title_change)
275
276                 self._loadingBanner = banners.GenericBanner()
277
278                 modelTypes, columns = zip(*self._get_columns())
279
280                 self._model = gtk.ListStore(*modelTypes)
281
282                 self._treeView = gtk.TreeView()
283                 self._treeView.connect("row-activated", self._on_row_activated)
284                 self._treeView.set_property("fixed-height-mode", True)
285                 self._treeView.set_headers_visible(False)
286                 self._treeView.set_model(self._model)
287                 for column in columns:
288                         if column is not None:
289                                 self._treeView.append_column(column)
290
291                 self._viewport = gtk.Viewport()
292                 self._viewport.add(self._treeView)
293
294                 self._treeScroller = gtk.ScrolledWindow()
295                 self._treeScroller.add(self._viewport)
296                 self._treeScroller.set_policy(gtk.POLICY_NEVER, gtk.POLICY_AUTOMATIC)
297                 self._treeScroller = hildonize.hildonize_scrollwindow(self._treeScroller)
298
299                 self._separator = gtk.HSeparator()
300                 self._presenter = presenter.NavControl(self._player, self._store)
301                 self.connect_auto(self._presenter, "home", self._on_home)
302                 self.connect_auto(self._presenter, "jump-to", self._on_jump)
303
304                 self._contentLayout = gtk.VBox(False)
305                 self._contentLayout.pack_start(self._treeScroller, True, True)
306                 self._contentLayout.pack_start(self._separator, False, True)
307                 self._contentLayout.pack_start(self._presenter.toplevel, False, True)
308
309                 self._layout.pack_start(self._loadingBanner.toplevel, False, False)
310                 self._layout.pack_start(self._contentLayout, True, True)
311
312         def show(self):
313                 BasicWindow.show(self)
314
315                 self._errorBanner.toplevel.hide()
316                 self._loadingBanner.toplevel.hide()
317
318                 self._refresh()
319                 self._presenter.refresh()
320
321         @classmethod
322         def _get_columns(cls):
323                 raise NotImplementedError("")
324
325         def _get_current_row(self):
326                 if self._player.node is None:
327                         return -1
328                 ancestors, current, descendants = stream_index.common_paths(self._player.node, self._node)
329                 if not descendants:
330                         return -1
331                 activeChild = descendants[0]
332                 for i, row in enumerate(self._model):
333                         if activeChild is row[0]:
334                                 return i
335                 else:
336                         return -1
337
338         def jump_to(self, node):
339                 ancestors, current, descendants = stream_index.common_paths(node, self._node)
340                 if current is None:
341                         raise RuntimeError("Cannot jump to node %s" % node)
342                 if not descendants:
343                         _moduleLogger.info("Current node is the target")
344                         return
345                 child = descendants[0]
346                 window = self._window_from_node(child)
347                 window.jump_to(node)
348
349         def _window_from_node(self, node):
350                 raise NotImplementedError("")
351
352         @misc_utils.log_exception(_moduleLogger)
353         def _on_row_activated(self, view, path, column):
354                 itr = self._model.get_iter(path)
355                 node = self._model.get_value(itr, 0)
356                 self._window_from_node(node)
357
358         @misc_utils.log_exception(_moduleLogger)
359         def _on_player_title_change(self, player, node):
360                 assert not self._isDestroyed
361                 self._select_row()
362
363         @misc_utils.log_exception(_moduleLogger)
364         def _on_jump(self, source, node):
365                 ancestors, current, descendants = stream_index.common_paths(node, self._node)
366                 if current is None:
367                         _moduleLogger.info("%s is not the target, moving up" % self._node)
368                         self.emit("jump-to", node)
369                         self._window.destroy()
370                         return
371                 if not descendants:
372                         _moduleLogger.info("Current node is the target")
373                         return
374                 child = descendants[0]
375                 window = self._window_from_node(child)
376                 window.jump_to(node)
377
378         @misc_utils.log_exception(_moduleLogger)
379         def _on_delay_scroll(self, *args):
380                 self._scroll_to_row()
381
382         def _show_loading(self):
383                 animationPath = self._store.STORE_LOOKUP["loading"]
384                 animation = self._store.get_pixbuf_animation_from_store(animationPath)
385                 self._loadingBanner.show(animation, "Loading...")
386
387         def _hide_loading(self):
388                 self._loadingBanner.hide()
389
390         def _refresh(self):
391                 self._show_loading()
392                 self._model.clear()
393
394         def _select_row(self):
395                 rowIndex = self._get_current_row()
396                 if rowIndex < 0:
397                         return
398                 path = (rowIndex, )
399                 self._treeView.get_selection().select_path(path)
400
401         def _scroll_to_row(self):
402                 rowIndex = self._get_current_row()
403                 if rowIndex < 0:
404                         return
405
406                 path = (rowIndex, )
407                 self._treeView.scroll_to_cell(path)
408
409                 treeViewHeight = self._treeView.get_allocation().height
410                 viewportHeight = self._viewport.get_allocation().height
411
412                 viewsPerPort = treeViewHeight / float(viewportHeight)
413                 maxRows = len(self._model)
414                 percentThrough = rowIndex / float(maxRows)
415                 dxByIndex = int(viewsPerPort * percentThrough * viewportHeight)
416
417                 dxMax = max(treeViewHeight - viewportHeight, 0)
418
419                 dx = min(dxByIndex, dxMax)
420                 adjustment = self._treeScroller.get_vadjustment()
421                 adjustment.value = dx
422
423
424 class PresenterWindow(BasicWindow):
425
426         def __init__(self, app, player, store, node):
427                 BasicWindow.__init__(self, app, player, store)
428                 self._node = node
429                 self._playerNode = self._player.node
430                 self._nextSearch = None
431                 self._updateSeek = None
432
433                 self.connect_auto(self._player, "state-change", self._on_player_state_change)
434                 self.connect_auto(self._player, "title-change", self._on_player_title_change)
435                 self.connect_auto(self._player, "error", self._on_player_error)
436
437                 self._loadingBanner = banners.GenericBanner()
438
439                 self._presenter = presenter.StreamPresenter(self._store)
440                 self._presenter.set_context(
441                         self._get_background(
442                                 gtk.ORIENTATION_VERTICAL if self._isPortrait else gtk.ORIENTATION_HORIZONTAL
443                         ),
444                         self._node.title,
445                         self._node.subtitle,
446                 )
447                 self._presenterNavigation = presenter.NavigationBox()
448                 self._presenterNavigation.toplevel.add(self._presenter.toplevel)
449                 self.connect_auto(self._presenterNavigation, "action", self._on_nav_action)
450                 self.connect_auto(self._presenterNavigation, "navigating", self._on_navigating)
451
452                 self._seekbar = hildonize.create_seekbar()
453                 self._seekbar.connect("change-value", self._on_user_seek)
454
455                 self._layout.pack_start(self._loadingBanner.toplevel, False, False)
456                 self._layout.pack_start(self._presenterNavigation.toplevel, True, True)
457                 self._layout.pack_start(self._seekbar, False, False)
458
459                 self._window.set_title(self._node.get_parent().title)
460
461         def _get_background(self, orientation):
462                 raise NotImplementedError()
463
464         def show(self):
465                 BasicWindow.show(self)
466                 self._window.show_all()
467                 self._errorBanner.toplevel.hide()
468                 self._loadingBanner.toplevel.hide()
469                 self._set_context(self._player.state)
470                 self._seekbar.hide()
471
472         def jump_to(self, node):
473                 assert self._node is node
474
475         def set_orientation(self, orientation):
476                 didChange = BasicWindow.set_orientation(self, orientation)
477                 if didChange:
478                         self._presenter.set_orientation(orientation)
479                         self._presenter.set_context(
480                                 self._get_background(orientation),
481                                 self._node.title,
482                                 self._node.subtitle,
483                         )
484                 return didChange
485
486         @property
487         def _active(self):
488                 return self._playerNode is self._node
489
490         def _show_loading(self):
491                 animationPath = self._store.STORE_LOOKUP["loading"]
492                 animation = self._store.get_pixbuf_animation_from_store(animationPath)
493                 self._loadingBanner.show(animation, "Loading...")
494
495         def _hide_loading(self):
496                 self._loadingBanner.hide()
497
498         def _set_context(self, state):
499                 if state == self._player.STATE_PLAY:
500                         if self._active:
501                                 self._presenter.set_state(self._store.STORE_LOOKUP["pause"])
502                         else:
503                                 self._presenter.set_state(self._store.STORE_LOOKUP["play"])
504                 elif state == self._player.STATE_PAUSE:
505                         self._presenter.set_state(self._store.STORE_LOOKUP["play"])
506                 elif state == self._player.STATE_STOP:
507                         self._presenter.set_state(self._store.STORE_LOOKUP["play"])
508                 else:
509                         _moduleLogger.info("Unhandled player state %s" % state)
510
511         @misc_utils.log_exception(_moduleLogger)
512         def _on_user_seek(self, widget, scroll, value):
513                 self._player.seek(value / 100.0)
514
515         @misc_utils.log_exception(_moduleLogger)
516         def _on_player_update_seek(self):
517                 if self._isDestroyed:
518                         return False
519                 self._seekbar.set_value(self._player.percent_elapsed * 100)
520                 return True
521
522         @misc_utils.log_exception(_moduleLogger)
523         def _on_player_state_change(self, player, newState):
524                 assert not self._isDestroyed
525                 if self._active and self._player.state == self._player.STATE_PLAY:
526                         self._seekbar.show()
527                         assert self._updateSeek is None
528                         self._updateSeek = go_utils.Timeout(self._on_player_update_seek, once=False)
529                         self._updateSeek.start(seconds=1)
530                 else:
531                         self._seekbar.hide()
532                         if self._updateSeek is not None:
533                                 self._updateSeek.cancel()
534                                 self._updateSeek = None
535
536                 if not self._presenterNavigation.is_active():
537                         self._set_context(newState)
538
539         @misc_utils.log_exception(_moduleLogger)
540         def _on_player_title_change(self, player, node):
541                 assert not self._isDestroyed
542                 if not self._active or node in [None, self._node]:
543                         self._playerNode = node
544                         return
545                 self._playerNode = node
546                 self.emit("jump-to", node)
547                 self._window.destroy()
548
549         @misc_utils.log_exception(_moduleLogger)
550         def _on_player_error(self, player, err, debug):
551                 assert not self._isDestroyed
552                 _moduleLogger.error("%r - %r" % (err, debug))
553                 self._errorBanner.push_message(err)
554
555         @misc_utils.log_exception(_moduleLogger)
556         def _on_navigating(self, widget, navState):
557                 if navState == "clicking":
558                         if self._player.state == self._player.STATE_PLAY:
559                                 if self._active:
560                                         imageName = "pause_pressed"
561                                 else:
562                                         imageName = "play_pressed"
563                         elif self._player.state == self._player.STATE_PAUSE:
564                                 imageName = "play_pressed"
565                         elif self._player.state == self._player.STATE_STOP:
566                                 imageName = "play_pressed"
567                         else:
568                                 _moduleLogger.info("Unhandled player state %s" % self._player.state)
569                 elif navState == "down":
570                         imageName = "home"
571                 elif navState == "up":
572                         if self._player.state == self._player.STATE_PLAY:
573                                 if self._active:
574                                         imageName = "pause"
575                                 else:
576                                         imageName = "play"
577                         elif self._player.state == self._player.STATE_PAUSE:
578                                 imageName = "play"
579                         elif self._player.state == self._player.STATE_STOP:
580                                 imageName = "play"
581                         else:
582                                 _moduleLogger.info("Unhandled player state %s" % self._player.state)
583                 elif navState == "left":
584                         imageName = "next"
585                 elif navState == "right":
586                         imageName = "prev"
587
588                 self._presenter.set_state(self._store.STORE_LOOKUP[imageName])
589
590         @misc_utils.log_exception(_moduleLogger)
591         def _on_nav_action(self, widget, navState):
592                 self._set_context(self._player.state)
593
594                 if navState == "clicking":
595                         if self._player.state == self._player.STATE_PLAY:
596                                 if self._active:
597                                         self._player.pause()
598                                 else:
599                                         self._player.set_piece_by_node(self._node)
600                                         self._player.play()
601                         elif self._player.state == self._player.STATE_PAUSE:
602                                 self._player.play()
603                         elif self._player.state == self._player.STATE_STOP:
604                                 self._player.set_piece_by_node(self._node)
605                                 self._player.play()
606                         else:
607                                 _moduleLogger.info("Unhandled player state %s" % self._player.state)
608                 elif navState == "down":
609                         self.emit("home")
610                         self._window.destroy()
611                 elif navState == "up":
612                         pass
613                 elif navState == "left":
614                         if self._active:
615                                 self._player.next()
616                         else:
617                                 assert self._nextSearch is None
618                                 self._nextSearch = stream_index.AsyncWalker(stream_index.get_next)
619                                 self._nextSearch.start(self._node, self._on_next_node, self._on_node_search_error)
620                 elif navState == "right":
621                         if self._active:
622                                 self._player.back()
623                         else:
624                                 assert self._nextSearch is None
625                                 self._nextSearch = stream_index.AsyncWalker(stream_index.get_previous)
626                                 self._nextSearch.start(self._node, self._on_next_node, self._on_node_search_error)
627
628         @misc_utils.log_exception(_moduleLogger)
629         def _on_next_node(self, node):
630                 self._nextSearch = None
631                 self.emit("jump-to", node)
632                 self._window.destroy()
633
634         @misc_utils.log_exception(_moduleLogger)
635         def _on_node_search_error(self, e):
636                 self._nextSearch = None
637                 self._errorBanner.push_message(str(e))
638
639
640 class AboutWindow(BasicWindow):
641
642         def __init__(self, app, player, store):
643                 BasicWindow.__init__(self, app, player, store)
644                 self._window.set_title(constants.__pretty_app_name__)
645
646                 self._titleLabel = gtk.Label()
647                 self._titleLabel.set_markup("""
648 <big><b>Waters of Shiloah</b></big>
649 <i>Maemo Edition</i>
650 Version %s
651 """ % (constants.__version__, ))
652                 self._titleLabel.set_property("justify", gtk.JUSTIFY_CENTER)
653
654                 self._copyLabel = gtk.Label()
655                 self._copyLabel.set_markup("""
656 <small>Developed by: Ed Page
657 Images by: Various Sources, See COPYING for author and license information (mix of various CC licenses, commercial, and non-commercial
658 This application nor various images are not endorsed by The Church of Jesus Christ of Latter-day Saints</small>
659 """)
660                 self._copyLabel.set_property("justify", gtk.JUSTIFY_CENTER)
661
662                 self._linkButton = gtk.LinkButton("http://watersofshiloah.garage.maemo.org")
663                 self._linkButton.set_label("Waters of Shiloah")
664                 self._linkButton.connect("clicked", self._on_website)
665
666                 self._radioLinkButton = gtk.LinkButton("http://radio.lds.org")
667                 self._radioLinkButton.set_label("Mormon Channel")
668                 self._radioLinkButton.connect("clicked", self._on_website)
669
670                 self._ldsLinkButton = gtk.LinkButton("http://www.lds.org")
671                 self._ldsLinkButton.set_label("LDS.org")
672                 self._ldsLinkButton.connect("clicked", self._on_website)
673
674                 self._spacedLayout = gtk.VBox(True)
675                 self._spacedLayout.pack_start(self._titleLabel, False, False)
676                 self._spacedLayout.pack_start(self._copyLabel, False, False)
677                 self._spacedLayout.pack_start(self._linkButton, False, False)
678                 self._spacedLayout.pack_start(self._radioLinkButton, False, False)
679                 self._spacedLayout.pack_start(self._ldsLinkButton, False, False)
680
681                 self._separator = gtk.HSeparator()
682                 self._presenter = presenter.NavControl(self._player, self._store)
683                 self.connect_auto(self._presenter, "home", self._on_home)
684                 self.connect_auto(self._presenter, "jump-to", self._on_jump)
685
686                 self._layout.pack_start(self._spacedLayout, True, True)
687                 self._layout.pack_start(self._presenter.toplevel, False, True)
688
689         def show(self):
690                 BasicWindow.show(self)
691                 self._window.show_all()
692                 self._errorBanner.toplevel.hide()
693                 self._presenter.refresh()
694
695         @misc_utils.log_exception(_moduleLogger)
696         def _on_about(self, *args):
697                 pass
698
699         @misc_utils.log_exception(_moduleLogger)
700         def _on_website(self, widget):
701                 uri = widget.get_uri()
702                 webbrowser.open(uri)
703
704         @misc_utils.log_exception(_moduleLogger)
705         def _on_jump(self, source, node):
706                 self.emit("jump-to", node)
707                 self._window.destroy()