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