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