Slight about change
[watersofshiloah] / src / windows / _base.py
index d876b72..6983439 100644 (file)
@@ -2,6 +2,7 @@ from __future__ import with_statement
 
 import ConfigParser
 import logging
+import webbrowser
 
 import gobject
 import gtk
@@ -13,7 +14,7 @@ import util.go_utils as go_utils
 
 import stream_index
 import banners
-import playcontrol
+import presenter
 
 
 _moduleLogger = logging.getLogger(__name__)
@@ -47,11 +48,17 @@ class BasicWindow(gobject.GObject, go_utils.AutoSignal):
                        gobject.TYPE_NONE,
                        (gobject.TYPE_BOOLEAN, ),
                ),
+               'rotate' : (
+                       gobject.SIGNAL_RUN_LAST,
+                       gobject.TYPE_NONE,
+                       (gobject.TYPE_PYOBJECT, ),
+               ),
        }
 
        def __init__(self, app, player, store):
                gobject.GObject.__init__(self)
                self._isDestroyed = False
+               self._isPortrait = True
 
                self._app = app
                self._player = player
@@ -63,18 +70,42 @@ class BasicWindow(gobject.GObject, go_utils.AutoSignal):
                self._errorBanner = banners.StackingBanner()
 
                self._layout = gtk.VBox()
-               self._layout.pack_start(self._errorBanner.toplevel, False, True)
 
                self._window = gtk.Window()
-               go_utils.AutoSignal.__init__(self, self.window)
                self._window.add(self._layout)
                self._window = hildonize.hildonize_window(self._app, self._window)
+               go_utils.AutoSignal.__init__(self, self.window)
 
                self._window.set_icon(self._store.get_pixbuf_from_store(self._store.STORE_LOOKUP["icon"]))
                self._window.connect("key-press-event", self._on_key_press)
                self._window.connect("window-state-event", self._on_window_state_change)
                self._window.connect("destroy", self._on_destroy)
 
+               if hildonize.GTK_MENU_USED:
+                       aboutMenuItem = gtk.MenuItem("About")
+                       aboutMenuItem.connect("activate", self._on_about)
+
+                       helpMenu = gtk.Menu()
+                       helpMenu.append(aboutMenuItem)
+
+                       helpMenuItem = gtk.MenuItem("Help")
+                       helpMenuItem.set_submenu(helpMenu)
+
+                       menuBar = gtk.MenuBar()
+                       menuBar.append(helpMenuItem)
+
+                       self._layout.pack_start(menuBar, False, False)
+               else:
+                       aboutMenuItem = gtk.Button("About")
+                       aboutMenuItem.connect("clicked", self._on_about)
+
+                       appMenu = hildonize.hildon.AppMenu()
+                       appMenu.append(aboutMenuItem)
+                       appMenu.show_all()
+                       self._window.set_app_menu(appMenu)
+
+               self._layout.pack_start(self._errorBanner.toplevel, False, True)
+
        @property
        def window(self):
                return self._window
@@ -89,7 +120,7 @@ class BasicWindow(gobject.GObject, go_utils.AutoSignal):
 
        def load_settings(self, config, sectionName):
                try:
-                       self._windowInFullscreen = config.getboolean(sectionName, "fullscreen")
+                       windowInFullscreen = config.getboolean(sectionName, "fullscreen")
                except ConfigParser.NoSectionError, e:
                        _moduleLogger.info(
                                "Settings file %s is missing section %s" % (
@@ -97,8 +128,9 @@ class BasicWindow(gobject.GObject, go_utils.AutoSignal):
                                        e.section,
                                )
                        )
+                       windowInFullscreen = self._windowInFullscreen
 
-               if self._windowInFullscreen:
+               if windowInFullscreen:
                        self._window.fullscreen()
                else:
                        self._window.unfullscreen()
@@ -106,17 +138,59 @@ class BasicWindow(gobject.GObject, go_utils.AutoSignal):
        def jump_to(self, node):
                raise NotImplementedError("On %s" % self)
 
+       def set_orientation(self, orientation):
+               oldIsPortrait = self._isPortrait
+               if orientation == gtk.ORIENTATION_VERTICAL:
+                       hildonize.window_to_portrait(self._window)
+                       self._isPortrait = True
+               elif orientation == gtk.ORIENTATION_HORIZONTAL:
+                       hildonize.window_to_landscape(self._window)
+                       self._isPortrait = False
+               else:
+                       raise NotImplementedError(orientation)
+               didChange = oldIsPortrait != self._isPortrait
+               if didChange:
+                       self.emit("rotate", orientation)
+               return didChange
+
+       def _configure_child(self, childWindow):
+               if not hildonize.IS_FREMANTLE_SUPPORTED:
+                       childWindow.window.set_modal(True)
+                       childWindow.window.set_transient_for(self._window)
+               childWindow.window.set_default_size(*self._window.get_size())
+               if self._windowInFullscreen:
+                       childWindow.window.fullscreen()
+               else:
+                       childWindow.window.unfullscreen()
+               childWindow.set_orientation(
+                       gtk.ORIENTATION_VERTICAL if self._isPortrait else gtk.ORIENTATION_HORIZONTAL
+               )
+               childWindow.connect_auto(childWindow, "quit", self._on_quit)
+               childWindow.connect_auto(childWindow, "home", self._on_home)
+               childWindow.connect_auto(childWindow, "jump-to", self._on_jump)
+               childWindow.connect_auto(childWindow, "fullscreen", self._on_child_fullscreen)
+               childWindow.connect_auto(childWindow, "rotate", self._on_child_rotate)
+
+       @misc_utils.log_exception(_moduleLogger)
+       def _on_about(self, *args):
+               sourceWindow = AboutWindow(self._app, self._player, self._store)
+               self._configure_child(sourceWindow)
+               sourceWindow.show()
+
        @misc_utils.log_exception(_moduleLogger)
        def _on_destroy(self, *args):
                self._isDestroyed = True
 
        @misc_utils.log_exception(_moduleLogger)
        def _on_window_state_change(self, widget, event, *args):
+               oldIsFull = self._windowInFullscreen
                if event.new_window_state & gtk.gdk.WINDOW_STATE_FULLSCREEN:
                        self._windowInFullscreen = True
                else:
                        self._windowInFullscreen = False
-               self.emit("fullscreen", self._windowInFullscreen)
+               if oldIsFull != self._windowInFullscreen:
+                       _moduleLogger.info("%r Emit fullscreen %s" % (self, self._windowInFullscreen))
+                       self.emit("fullscreen", self._windowInFullscreen)
 
        @misc_utils.log_exception(_moduleLogger)
        def _on_key_press(self, widget, event, *args):
@@ -132,6 +206,12 @@ class BasicWindow(gobject.GObject, go_utils.AutoSignal):
                        else:
                                self._window.fullscreen ()
                        return True
+               elif event.keyval == gtk.keysyms.o and event.get_state() & gtk.gdk.CONTROL_MASK:
+                       if self._isPortrait:
+                               self.set_orientation(gtk.ORIENTATION_HORIZONTAL)
+                       else:
+                               self.set_orientation(gtk.ORIENTATION_VERTICAL)
+                       return True
                elif (
                        event.keyval in (gtk.keysyms.w, ) and
                        event.get_state() & gtk.gdk.CONTROL_MASK
@@ -142,6 +222,7 @@ class BasicWindow(gobject.GObject, go_utils.AutoSignal):
                        event.get_state() & gtk.gdk.CONTROL_MASK
                ):
                        self.emit("quit")
+                       self._window.destroy()
                elif event.keyval == gtk.keysyms.l and event.get_state() & gtk.gdk.CONTROL_MASK:
                        with open(constants._user_logpath_, "r") as f:
                                logLines = f.xreadlines()
@@ -155,6 +236,19 @@ class BasicWindow(gobject.GObject, go_utils.AutoSignal):
                self._window.destroy()
 
        @misc_utils.log_exception(_moduleLogger)
+       def _on_child_fullscreen(self, source, isFull):
+               if isFull:
+                       _moduleLogger.info("Full screen %r to mirror child %r" % (self, source))
+                       self._window.fullscreen()
+               else:
+                       _moduleLogger.info("Unfull screen %r to mirror child %r" % (self, source))
+                       self._window.unfullscreen()
+
+       @misc_utils.log_exception(_moduleLogger)
+       def _on_child_rotate(self, source, orientation):
+               self.set_orientation(orientation)
+
+       @misc_utils.log_exception(_moduleLogger)
        def _on_jump(self, source, node):
                raise NotImplementedError("On %s" % self)
 
@@ -187,19 +281,23 @@ class ListWindow(BasicWindow):
                        if column is not None:
                                self._treeView.append_column(column)
 
+               self._viewport = gtk.Viewport()
+               self._viewport.add(self._treeView)
+
                self._treeScroller = gtk.ScrolledWindow()
-               self._treeScroller.add(self._treeView)
+               self._treeScroller.add(self._viewport)
                self._treeScroller.set_policy(gtk.POLICY_NEVER, gtk.POLICY_AUTOMATIC)
+               self._treeScroller = hildonize.hildonize_scrollwindow(self._treeScroller)
 
                self._separator = gtk.HSeparator()
-               self._playcontrol = playcontrol.NavControl(self._player, self._store)
-               self._playcontrol.connect("home", self._on_home)
-               self._playcontrol.connect("jump-to", self._on_jump)
+               self._presenter = presenter.NavControl(self._player, self._store)
+               self.connect_auto(self._presenter, "home", self._on_home)
+               self.connect_auto(self._presenter, "jump-to", self._on_jump)
 
                self._contentLayout = gtk.VBox(False)
                self._contentLayout.pack_start(self._treeScroller, True, True)
                self._contentLayout.pack_start(self._separator, False, True)
-               self._contentLayout.pack_start(self._playcontrol.toplevel, False, True)
+               self._contentLayout.pack_start(self._presenter.toplevel, False, True)
 
                self._layout.pack_start(self._loadingBanner.toplevel, False, False)
                self._layout.pack_start(self._contentLayout, True, True)
@@ -211,7 +309,7 @@ class ListWindow(BasicWindow):
                self._loadingBanner.toplevel.hide()
 
                self._refresh()
-               self._playcontrol.refresh()
+               self._presenter.refresh()
 
        @classmethod
        def _get_columns(cls):
@@ -246,10 +344,13 @@ class ListWindow(BasicWindow):
 
        @misc_utils.log_exception(_moduleLogger)
        def _on_row_activated(self, view, path, column):
-               raise NotImplementedError("")
+               itr = self._model.get_iter(path)
+               node = self._model.get_value(itr, 0)
+               self._window_from_node(node)
 
        @misc_utils.log_exception(_moduleLogger)
        def _on_player_title_change(self, player, node):
+               assert not self._isDestroyed
                self._select_row()
 
        @misc_utils.log_exception(_moduleLogger)
@@ -267,6 +368,10 @@ class ListWindow(BasicWindow):
                window = self._window_from_node(child)
                window.jump_to(node)
 
+       @misc_utils.log_exception(_moduleLogger)
+       def _on_delay_scroll(self, *args):
+               self._scroll_to_row()
+
        def _show_loading(self):
                animationPath = self._store.STORE_LOOKUP["loading"]
                animation = self._store.get_pixbuf_animation_from_store(animationPath)
@@ -284,5 +389,299 @@ class ListWindow(BasicWindow):
                if rowIndex < 0:
                        return
                path = (rowIndex, )
-               self._treeView.scroll_to_cell(path)
                self._treeView.get_selection().select_path(path)
+
+       def _scroll_to_row(self):
+               rowIndex = self._get_current_row()
+               if rowIndex < 0:
+                       return
+
+               path = (rowIndex, )
+               self._treeView.scroll_to_cell(path)
+
+               treeViewHeight = self._treeView.get_allocation().height
+               viewportHeight = self._viewport.get_allocation().height
+
+               viewsPerPort = treeViewHeight / float(viewportHeight)
+               maxRows = len(self._model)
+               percentThrough = rowIndex / float(maxRows)
+               dxByIndex = int(viewsPerPort * percentThrough * viewportHeight)
+
+               dxMax = max(treeViewHeight - viewportHeight, 0)
+
+               dx = min(dxByIndex, dxMax)
+               adjustment = self._treeScroller.get_vadjustment()
+               adjustment.value = dx
+
+
+class PresenterWindow(BasicWindow):
+
+       def __init__(self, app, player, store, node):
+               BasicWindow.__init__(self, app, player, store)
+               self._node = node
+               self._playerNode = self._player.node
+               self._nextSearch = None
+               self._updateSeek = None
+
+               self.connect_auto(self._player, "state-change", self._on_player_state_change)
+               self.connect_auto(self._player, "title-change", self._on_player_title_change)
+               self.connect_auto(self._player, "error", self._on_player_error)
+
+               self._loadingBanner = banners.GenericBanner()
+
+               self._presenter = presenter.StreamPresenter(self._store)
+               self._presenter.set_context(
+                       self._get_background(
+                               gtk.ORIENTATION_VERTICAL if self._isPortrait else gtk.ORIENTATION_HORIZONTAL
+                       ),
+                       self._node.title,
+                       self._node.subtitle,
+               )
+               self._presenterNavigation = presenter.NavigationBox()
+               self._presenterNavigation.toplevel.add(self._presenter.toplevel)
+               self.connect_auto(self._presenterNavigation, "action", self._on_nav_action)
+               self.connect_auto(self._presenterNavigation, "navigating", self._on_navigating)
+
+               self._seekbar = hildonize.create_seekbar()
+               self._seekbar.connect("change-value", self._on_user_seek)
+
+               self._layout.pack_start(self._loadingBanner.toplevel, False, False)
+               self._layout.pack_start(self._presenterNavigation.toplevel, True, True)
+               self._layout.pack_start(self._seekbar, False, False)
+
+               self._window.set_title(self._node.get_parent().title)
+
+       def _get_background(self, orientation):
+               raise NotImplementedError()
+
+       def show(self):
+               BasicWindow.show(self)
+               self._window.show_all()
+               self._errorBanner.toplevel.hide()
+               self._loadingBanner.toplevel.hide()
+               self._set_context(self._player.state)
+               self._seekbar.hide()
+
+       def jump_to(self, node):
+               assert self._node is node
+
+       def set_orientation(self, orientation):
+               didChange = BasicWindow.set_orientation(self, orientation)
+               if didChange:
+                       self._presenter.set_orientation(orientation)
+                       self._presenter.set_context(
+                               self._get_background(orientation),
+                               self._node.title,
+                               self._node.subtitle,
+                       )
+
+       @property
+       def _active(self):
+               return self._playerNode is self._node
+
+       def _show_loading(self):
+               animationPath = self._store.STORE_LOOKUP["loading"]
+               animation = self._store.get_pixbuf_animation_from_store(animationPath)
+               self._loadingBanner.show(animation, "Loading...")
+
+       def _hide_loading(self):
+               self._loadingBanner.hide()
+
+       def _set_context(self, state):
+               if state == self._player.STATE_PLAY:
+                       if self._active:
+                               self._presenter.set_state(self._store.STORE_LOOKUP["pause"])
+                       else:
+                               self._presenter.set_state(self._store.STORE_LOOKUP["play"])
+               elif state == self._player.STATE_PAUSE:
+                       self._presenter.set_state(self._store.STORE_LOOKUP["play"])
+               elif state == self._player.STATE_STOP:
+                       self._presenter.set_state(self._store.STORE_LOOKUP["play"])
+               else:
+                       _moduleLogger.info("Unhandled player state %s" % state)
+
+       @misc_utils.log_exception(_moduleLogger)
+       def _on_user_seek(self, widget, scroll, value):
+               self._player.seek(value / 100.0)
+
+       @misc_utils.log_exception(_moduleLogger)
+       def _on_player_update_seek(self):
+               if self._isDestroyed:
+                       return False
+               self._seekbar.set_value(self._player.percent_elapsed * 100)
+               return True
+
+       @misc_utils.log_exception(_moduleLogger)
+       def _on_player_state_change(self, player, newState):
+               assert not self._isDestroyed
+               if self._active and self._player.state == self._player.STATE_PLAY:
+                       self._seekbar.show()
+                       assert self._updateSeek is None
+                       self._updateSeek = go_utils.Timeout(self._on_player_update_seek, once=False)
+                       self._updateSeek.start(seconds=1)
+               else:
+                       self._seekbar.hide()
+                       if self._updateSeek is not None:
+                               self._updateSeek.cancel()
+                               self._updateSeek = None
+
+               if not self._presenterNavigation.is_active():
+                       self._set_context(newState)
+
+       @misc_utils.log_exception(_moduleLogger)
+       def _on_player_title_change(self, player, node):
+               assert not self._isDestroyed
+               if not self._active or node in [None, self._node]:
+                       self._playerNode = node
+                       return
+               self._playerNode = node
+               self.emit("jump-to", node)
+               self._window.destroy()
+
+       @misc_utils.log_exception(_moduleLogger)
+       def _on_player_error(self, player, err, debug):
+               assert not self._isDestroyed
+               _moduleLogger.error("%r - %r" % (err, debug))
+
+       @misc_utils.log_exception(_moduleLogger)
+       def _on_navigating(self, widget, navState):
+               if navState == "clicking":
+                       if self._player.state == self._player.STATE_PLAY:
+                               if self._active:
+                                       imageName = "pause_pressed"
+                               else:
+                                       imageName = "play_pressed"
+                       elif self._player.state == self._player.STATE_PAUSE:
+                               imageName = "play_pressed"
+                       elif self._player.state == self._player.STATE_STOP:
+                               imageName = "play_pressed"
+                       else:
+                               _moduleLogger.info("Unhandled player state %s" % self._player.state)
+               elif navState == "down":
+                       imageName = "home"
+               elif navState == "up":
+                       if self._player.state == self._player.STATE_PLAY:
+                               if self._active:
+                                       imageName = "pause"
+                               else:
+                                       imageName = "play"
+                       elif self._player.state == self._player.STATE_PAUSE:
+                               imageName = "play"
+                       elif self._player.state == self._player.STATE_STOP:
+                               imageName = "play"
+                       else:
+                               _moduleLogger.info("Unhandled player state %s" % self._player.state)
+               elif navState == "left":
+                       imageName = "next"
+               elif navState == "right":
+                       imageName = "prev"
+
+               self._presenter.set_state(self._store.STORE_LOOKUP[imageName])
+
+       @misc_utils.log_exception(_moduleLogger)
+       def _on_nav_action(self, widget, navState):
+               self._set_context(self._player.state)
+
+               if navState == "clicking":
+                       if self._player.state == self._player.STATE_PLAY:
+                               if self._active:
+                                       self._player.pause()
+                               else:
+                                       self._player.set_piece_by_node(self._node)
+                                       self._player.play()
+                       elif self._player.state == self._player.STATE_PAUSE:
+                               self._player.play()
+                       elif self._player.state == self._player.STATE_STOP:
+                               self._player.set_piece_by_node(self._node)
+                               self._player.play()
+                       else:
+                               _moduleLogger.info("Unhandled player state %s" % self._player.state)
+               elif navState == "down":
+                       self.emit("home")
+                       self._window.destroy()
+               elif navState == "up":
+                       pass
+               elif navState == "left":
+                       if self._active:
+                               self._player.next()
+                       else:
+                               assert self._nextSearch is None
+                               self._nextSearch = stream_index.AsyncWalker(stream_index.get_next)
+                               self._nextSearch.start(self._node, self._on_next_node, self._on_node_search_error)
+               elif navState == "right":
+                       if self._active:
+                               self._player.back()
+                       else:
+                               assert self._nextSearch is None
+                               self._nextSearch = stream_index.AsyncWalker(stream_index.get_previous)
+                               self._nextSearch.start(self._node, self._on_next_node, self._on_node_search_error)
+
+       @misc_utils.log_exception(_moduleLogger)
+       def _on_next_node(self, node):
+               self._nextSearch = None
+               self.emit("jump-to", node)
+               self._window.destroy()
+
+       @misc_utils.log_exception(_moduleLogger)
+       def _on_node_search_error(self, e):
+               self._nextSearch = None
+               self._errorBanner.push_message(str(e))
+
+
+class AboutWindow(BasicWindow):
+
+       def __init__(self, app, player, store):
+               BasicWindow.__init__(self, app, player, store)
+               self._window.set_title(constants.__pretty_app_name__)
+
+               self._titleLabel = gtk.Label()
+               self._titleLabel.set_markup("""
+<big><b>Mormon Channel</b></big>
+<i>Maemo Edition</i>
+Version %s
+""" % (constants.__version__, ))
+               self._titleLabel.set_property("justify", gtk.JUSTIFY_CENTER)
+
+               self._copyLabel = gtk.Label()
+               self._copyLabel.set_markup("""
+<small>(c) 2010 Intellectual Reserve, Inc.
+All rights reserved.</small>
+""")
+               self._copyLabel.set_property("justify", gtk.JUSTIFY_CENTER)
+
+               self._linkButton = gtk.LinkButton("http://www.lds.org")
+               self._linkButton.set_label("LDS.org")
+               self._linkButton.connect("clicked", self._on_website)
+
+               self._spacedLayout = gtk.VBox(True)
+               self._spacedLayout.pack_start(self._titleLabel, False, False)
+               self._spacedLayout.pack_start(self._copyLabel, False, False)
+               self._spacedLayout.pack_start(self._linkButton, False, False)
+
+               self._separator = gtk.HSeparator()
+               self._presenter = presenter.NavControl(self._player, self._store)
+               self.connect_auto(self._presenter, "home", self._on_home)
+               self.connect_auto(self._presenter, "jump-to", self._on_jump)
+
+               self._layout.pack_start(self._spacedLayout, True, True)
+               self._layout.pack_start(self._presenter.toplevel, False, True)
+
+       def show(self):
+               BasicWindow.show(self)
+               self._window.show_all()
+               self._errorBanner.toplevel.hide()
+               self._presenter.refresh()
+
+       @misc_utils.log_exception(_moduleLogger)
+       def _on_about(self, *args):
+               pass
+
+       @misc_utils.log_exception(_moduleLogger)
+       def _on_website(self, widget):
+               uri = widget.get_uri()
+               webbrowser.open(uri)
+
+       @misc_utils.log_exception(_moduleLogger)
+       def _on_jump(self, source, node):
+               self.emit("jump-to", node)
+               self._window.destroy()