Implementing Jump To with a couple of helpers along the way which also
authorEd Page <eopage@byu.net>
Thu, 13 May 2010 01:32:31 +0000 (20:32 -0500)
committerEd Page <eopage@byu.net>
Thu, 13 May 2010 03:11:19 +0000 (22:11 -0500)
simplified implementing highlighting the current row

src/index.py [deleted file]
src/mormonchannel_gtk.py
src/stream_index.py [new file with mode: 0644]
src/windows.py

diff --git a/src/index.py b/src/index.py
deleted file mode 100644 (file)
index 2ffb6c2..0000000
+++ /dev/null
@@ -1,364 +0,0 @@
-import weakref
-import logging
-
-import util.misc as misc_utils
-from util import go_utils
-import backend
-
-
-_moduleLogger = logging.getLogger(__name__)
-
-
-class Connection(object):
-
-       def __init__(self):
-               self._backend = backend.Backend()
-               self._indexing = go_utils.AsyncPool()
-
-       def start(self):
-               self._indexing.start()
-
-       def stop(self):
-               self._indexing.stop()
-
-       def download(self, func, on_success, on_error, args = None, kwds = None):
-               if args is None:
-                       args = ()
-               if kwds is None:
-                       kwds = {}
-
-               self._indexing.clear_tasks()
-               self._indexing.add_task(
-                       getattr(self._backend, func),
-                       args,
-                       kwds,
-                       on_success,
-                       on_error,
-               )
-
-
-class AudioIndex(object):
-
-       def __init__(self):
-               self._connection = Connection()
-               self._languages = None
-               self._languagesRequest = None
-               self._sources = {}
-
-       def start(self):
-               self._connection.start()
-
-       def stop(self):
-               self._connection.stop()
-
-       def get_languages(self, on_success, on_error):
-               if self._languages is None:
-                       assert self._languagesRequest is None
-                       self._languagesRequest = on_success, on_error
-                       self._connection.download(
-                               "get_languages",
-                               self._on_get_languages,
-                               self._on_languages_error
-                       )
-               else:
-                       on_success(self._languages)
-
-       def get_source(self, source, langId = None):
-               key = (source, langId)
-               if key in self._sources:
-                       node = self._sources[key]
-               else:
-                       if source == "radio":
-                               node = RadioNode(self._connection)
-                       elif source == "conferences":
-                               assert langId is not None
-                               node = ConferencesNode(self._connection, langId)
-                       else:
-                               raise NotImplementedError(source)
-                       self._sources[key] = node
-
-               return node
-
-       @misc_utils.log_exception(_moduleLogger)
-       def _on_get_languages(self, languages):
-               assert self._languages is None
-               assert self._languagesRequest is not None
-               r = self._languagesRequest
-               self._languagesRequest = None
-               self._languages = languages
-               r[0](self._languages)
-
-       @misc_utils.log_exception(_moduleLogger)
-       def _on_languages_error(self, e):
-               assert self._languages is None
-               assert self._languagesRequest is not None
-               r = self._languagesRequest
-               self._languagesRequest = None
-               r[1](self._languages)
-
-
-class Node(object):
-
-       def __init__(self, connection, parent, data):
-               self._connection = connection
-               self._parent = weakref.ref(parent) if parent is not None else None
-               self._data = data
-               self._children = None
-
-       def get_children(self, on_success, on_error):
-               if self._children is None:
-                       self._get_children(on_success, on_error)
-               else:
-                       on_success(self._children)
-
-       def get_parent(self):
-               if self._parent is None:
-                       raise RuntimeError("")
-               parent = self._parent()
-               return parent
-
-       def get_properties(self):
-               return self._data
-
-       @property
-       def title(self):
-               raise NotImplementedError("On %s" % type(self))
-
-       def is_leaf(self):
-               raise NotImplementedError("")
-
-       def _get_children(self, on_success, on_error):
-               raise NotImplementedError("")
-
-
-class ParentNode(Node):
-
-       def __init__(self, connection, parent, data):
-               Node.__init__(self, connection, parent, data)
-               self._request = None
-
-       def is_leaf(self):
-               return False
-
-       def _get_children(self, on_success, on_error):
-               assert self._request is None
-               assert self._children is None
-               self._request = on_success, on_error
-
-               func, args, kwds = self._get_func()
-
-               self._connection.download(
-                       func,
-                       self._on_success,
-                       self._on_error,
-                       args,
-                       kwds,
-               )
-
-       def _get_func(self):
-               raise NotImplementedError()
-
-       def _create_child(self, data):
-               raise NotImplementedError()
-
-       @misc_utils.log_exception(_moduleLogger)
-       def _on_success(self, data):
-               r = self._request
-               self._request = None
-               try:
-                       self._children = [
-                               self._create_child(child)
-                               for child in data
-                       ]
-               except Exception, e:
-                       _moduleLogger.exception("Translating error")
-                       self._children = None
-                       r[1](e)
-               else:
-                       r[0](self._children)
-
-       @misc_utils.log_exception(_moduleLogger)
-       def _on_error(self, error):
-               r = self._request
-               self._request = None
-               r[1](error)
-
-
-class LeafNode(Node):
-
-       def __init__(self, connection, parent, data):
-               Node.__init__(self, connection, parent, data)
-
-       def is_leaf(self):
-               return True
-
-       @property
-       def can_navigate(self):
-               raise NotImplementedError("On %s" % type(self))
-
-       @property
-       def subtitle(self):
-               raise NotImplementedError("On %s" % type(self))
-
-       @property
-       def uri(self):
-               raise NotImplementedError("On %s" % type(self))
-
-       def _get_children(self, on_success, on_error):
-               raise RuntimeError("Not is a leaf")
-
-
-class RadioNode(ParentNode):
-
-       def __init__(self, connection):
-               ParentNode.__init__(self, connection, None, {})
-
-       @property
-       def title(self):
-               return "Radio"
-
-       def _get_func(self):
-               return "get_radio_channels", (), {}
-
-       def _create_child(self, data):
-               return RadioChannelNode(self._connection, self, data)
-
-
-class RadioChannelNode(LeafNode):
-
-       def __init__(self, connection, parent, data):
-               LeafNode.__init__(self, connection, parent, data)
-               self._extendedData = {}
-               self._request = None
-
-       @property
-       def can_navigate(self):
-               return False
-
-       @property
-       def title(self):
-               return "Radio"
-
-       @property
-       def subtitle(self):
-               return ""
-
-       @property
-       def uri(self):
-               return self._data["url"]
-
-       def get_programming(self, date, on_success, on_error):
-               date = date.strftime("%Y-%m-%d")
-               try:
-                       programming = self._extendedData[date]
-               except KeyError:
-                       self._get_programming(date, on_success, on_error)
-               else:
-                       on_success(programming)
-
-       def _get_programming(self, date, on_success, on_error):
-               assert self._request is None
-               assert date not in self._extendedData
-               self._request = on_success, on_error, date
-
-               self._connection.download(
-                       "get_radio_channel_programming",
-                       self._on_success,
-                       self._on_error,
-                       (self._data["id"], date),
-                       {},
-               )
-
-       @misc_utils.log_exception(_moduleLogger)
-       def _on_success(self, data):
-               r = self._request
-               date = r[2]
-               self._request = None
-               try:
-                       self._extendedData[date] = [
-                               child
-                               for child in data
-                       ]
-               except Exception, e:
-                       _moduleLogger.exception("Translating error")
-                       del self._extendedData[date]
-                       r[1](e)
-               else:
-                       r[0](self._extendedData[date])
-
-       @misc_utils.log_exception(_moduleLogger)
-       def _on_error(self, error):
-               r = self._request
-               self._request = None
-               r[1](error)
-
-
-class ConferencesNode(ParentNode):
-
-       def __init__(self, connection, langId):
-               ParentNode.__init__(self, connection, None, {})
-               self._langId = langId
-
-       @property
-       def title(self):
-               return "Conferences"
-
-       def _get_func(self):
-               return "get_conferences", (self._langId, ), {}
-
-       def _create_child(self, data):
-               return ConferenceNode(self._connection, self, data)
-
-
-class ConferenceNode(ParentNode):
-
-       def __init__(self, connection, parent, data):
-               ParentNode.__init__(self, connection, parent, data)
-
-       @property
-       def title(self):
-               return self._data["title"]
-
-       def _get_func(self):
-               return "get_conference_sessions", (self._data["id"], ), {}
-
-       def _create_child(self, data):
-               return SessionNode(self._connection, self, data)
-
-
-class SessionNode(ParentNode):
-
-       def __init__(self, connection, parent, data):
-               ParentNode.__init__(self, connection, parent, data)
-
-       @property
-       def title(self):
-               return self._data["title"]
-
-       def _get_func(self):
-               return "get_conference_talks", (self._data["id"], ), {}
-
-       def _create_child(self, data):
-               return TalkNode(self._connection, self, data)
-
-
-class TalkNode(LeafNode):
-
-       def __init__(self, connection, parent, data):
-               LeafNode.__init__(self, connection, parent, data)
-
-       @property
-       def can_navigate(self):
-               return True
-
-       @property
-       def title(self):
-               return self._data["title"]
-
-       @property
-       def subtitle(self):
-               return self._data["speaker"]
-
-       @property
-       def uri(self):
-               return self._data["url"]
index aa31895..1b18191 100755 (executable)
@@ -2,8 +2,6 @@
 # -*- coding: utf-8 -*-
 
 """
-@todo Implement Jump TO
-@todo Implement highlighting of current track
 @todo Restructure so there is a windows/ folder with a file per source
 @todo Add additional sources
 @bug All connect's need disconnects or else we will leak a bunch of objects
@@ -31,7 +29,7 @@ import util.misc as misc_utils
 
 import imagestore
 import player
-import index
+import stream_index
 import windows
 
 
@@ -44,7 +42,7 @@ class MormonChannelProgram(hildonize.get_app_class()):
        def __init__(self):
                super(MormonChannelProgram, self).__init__()
                self._store = imagestore.ImageStore("../data", "../data")
-               self._index = index.AudioIndex()
+               self._index = stream_index.AudioIndex()
                self._player = player.Player(self._index)
 
                self._index.start()
diff --git a/src/stream_index.py b/src/stream_index.py
new file mode 100644 (file)
index 0000000..8e83769
--- /dev/null
@@ -0,0 +1,406 @@
+import weakref
+import logging
+
+import util.misc as misc_utils
+from util import go_utils
+import backend
+
+
+_moduleLogger = logging.getLogger(__name__)
+
+
+SOURCE_RADIO = "radio"
+SOURCE_CONFERENCES = "conferences"
+SOURCE_MAGAZINES = "magazines"
+SOURCE_SCRIPTURES = "scriptures"
+
+
+class Connection(object):
+
+       def __init__(self):
+               self._backend = backend.Backend()
+               self._indexing = go_utils.AsyncPool()
+
+       def start(self):
+               self._indexing.start()
+
+       def stop(self):
+               self._indexing.stop()
+
+       def download(self, func, on_success, on_error, args = None, kwds = None):
+               if args is None:
+                       args = ()
+               if kwds is None:
+                       kwds = {}
+
+               self._indexing.clear_tasks()
+               self._indexing.add_task(
+                       getattr(self._backend, func),
+                       args,
+                       kwds,
+                       on_success,
+                       on_error,
+               )
+
+
+class AudioIndex(object):
+
+       def __init__(self):
+               self._connection = Connection()
+               self._languages = None
+               self._languagesRequest = None
+               self._sources = {}
+
+       def start(self):
+               self._connection.start()
+
+       def stop(self):
+               self._connection.stop()
+
+       def get_languages(self, on_success, on_error):
+               if self._languages is None:
+                       assert self._languagesRequest is None
+                       self._languagesRequest = on_success, on_error
+                       self._connection.download(
+                               "get_languages",
+                               self._on_get_languages,
+                               self._on_languages_error
+                       )
+               else:
+                       on_success(self._languages)
+
+       def get_source(self, source, langId = None):
+               key = (source, langId)
+               if key in self._sources:
+                       node = self._sources[key]
+               else:
+                       if source == SOURCE_RADIO:
+                               node = RadioNode(self._connection)
+                       elif source == SOURCE_CONFERENCES:
+                               assert langId is not None
+                               node = ConferencesNode(self._connection, langId)
+                       else:
+                               raise NotImplementedError(source)
+                       self._sources[key] = node
+
+               return node
+
+       @misc_utils.log_exception(_moduleLogger)
+       def _on_get_languages(self, languages):
+               assert self._languages is None
+               assert self._languagesRequest is not None
+               r = self._languagesRequest
+               self._languagesRequest = None
+               self._languages = languages
+               r[0](self._languages)
+
+       @misc_utils.log_exception(_moduleLogger)
+       def _on_languages_error(self, e):
+               assert self._languages is None
+               assert self._languagesRequest is not None
+               r = self._languagesRequest
+               self._languagesRequest = None
+               r[1](self._languages)
+
+
+class Node(object):
+
+       def __init__(self, connection, parent, data, id):
+               self._connection = connection
+               self._parent = weakref.ref(parent) if parent is not None else None
+               self._data = data
+               self._children = None
+               self._id = id
+
+       def get_children(self, on_success, on_error):
+               if self._children is None:
+                       self._get_children(on_success, on_error)
+               else:
+                       on_success(self._children)
+
+       def get_parent(self):
+               if self._parent is None:
+                       raise RuntimeError("")
+               parent = self._parent()
+               return parent
+
+       def get_properties(self):
+               return self._data
+
+       @property
+       def title(self):
+               raise NotImplementedError("On %s" % type(self))
+
+       @property
+       def id(self):
+               return self._id
+
+       def is_leaf(self):
+               raise NotImplementedError("")
+
+       def _get_children(self, on_success, on_error):
+               raise NotImplementedError("")
+
+
+class ParentNode(Node):
+
+       def __init__(self, connection, parent, data, id):
+               Node.__init__(self, connection, parent, data, id)
+               self._request = None
+
+       def is_leaf(self):
+               return False
+
+       def _get_children(self, on_success, on_error):
+               assert self._request is None
+               assert self._children is None
+               self._request = on_success, on_error
+
+               func, args, kwds = self._get_func()
+
+               self._connection.download(
+                       func,
+                       self._on_success,
+                       self._on_error,
+                       args,
+                       kwds,
+               )
+
+       def _get_func(self):
+               raise NotImplementedError()
+
+       def _create_child(self, data, id):
+               raise NotImplementedError()
+
+       @misc_utils.log_exception(_moduleLogger)
+       def _on_success(self, data):
+               r = self._request
+               self._request = None
+               try:
+                       self._children = [
+                               self._create_child(child, i)
+                               for i, child in enumerate(data)
+                       ]
+               except Exception, e:
+                       _moduleLogger.exception("Translating error")
+                       self._children = None
+                       r[1](e)
+               else:
+                       r[0](self._children)
+
+       @misc_utils.log_exception(_moduleLogger)
+       def _on_error(self, error):
+               r = self._request
+               self._request = None
+               r[1](error)
+
+
+class LeafNode(Node):
+
+       def __init__(self, connection, parent, data, id):
+               Node.__init__(self, connection, parent, data, id)
+
+       def is_leaf(self):
+               return True
+
+       @property
+       def can_navigate(self):
+               raise NotImplementedError("On %s" % type(self))
+
+       @property
+       def subtitle(self):
+               raise NotImplementedError("On %s" % type(self))
+
+       @property
+       def uri(self):
+               raise NotImplementedError("On %s" % type(self))
+
+       def _get_children(self, on_success, on_error):
+               raise RuntimeError("Not is a leaf")
+
+
+class RadioNode(ParentNode):
+
+       def __init__(self, connection):
+               ParentNode.__init__(self, connection, None, {}, SOURCE_RADIO)
+
+       @property
+       def title(self):
+               return "Radio"
+
+       def _get_func(self):
+               return "get_radio_channels", (), {}
+
+       def _create_child(self, data, id):
+               return RadioChannelNode(self._connection, self, data, id)
+
+
+class RadioChannelNode(LeafNode):
+
+       def __init__(self, connection, parent, data, id):
+               LeafNode.__init__(self, connection, parent, data, id)
+               self._extendedData = {}
+               self._request = None
+
+       @property
+       def can_navigate(self):
+               return False
+
+       @property
+       def title(self):
+               return "Radio"
+
+       @property
+       def subtitle(self):
+               return ""
+
+       @property
+       def uri(self):
+               return self._data["url"]
+
+       def get_programming(self, date, on_success, on_error):
+               date = date.strftime("%Y-%m-%d")
+               try:
+                       programming = self._extendedData[date]
+               except KeyError:
+                       self._get_programming(date, on_success, on_error)
+               else:
+                       on_success(programming)
+
+       def _get_programming(self, date, on_success, on_error):
+               assert self._request is None
+               assert date not in self._extendedData
+               self._request = on_success, on_error, date
+
+               self._connection.download(
+                       "get_radio_channel_programming",
+                       self._on_success,
+                       self._on_error,
+                       (self._data["id"], date),
+                       {},
+               )
+
+       @misc_utils.log_exception(_moduleLogger)
+       def _on_success(self, data):
+               r = self._request
+               date = r[2]
+               self._request = None
+               try:
+                       self._extendedData[date] = [
+                               child
+                               for child in data
+                       ]
+               except Exception, e:
+                       _moduleLogger.exception("Translating error")
+                       del self._extendedData[date]
+                       r[1](e)
+               else:
+                       r[0](self._extendedData[date])
+
+       @misc_utils.log_exception(_moduleLogger)
+       def _on_error(self, error):
+               r = self._request
+               self._request = None
+               r[1](error)
+
+
+class ConferencesNode(ParentNode):
+
+       def __init__(self, connection, langId):
+               ParentNode.__init__(self, connection, None, {}, SOURCE_CONFERENCES)
+               self._langId = langId
+
+       @property
+       def title(self):
+               return "Conferences"
+
+       def _get_func(self):
+               return "get_conferences", (self._langId, ), {}
+
+       def _create_child(self, data, id):
+               return ConferenceNode(self._connection, self, data, id)
+
+
+class ConferenceNode(ParentNode):
+
+       def __init__(self, connection, parent, data, id):
+               ParentNode.__init__(self, connection, parent, data, id)
+
+       @property
+       def title(self):
+               return self._data["title"]
+
+       def _get_func(self):
+               return "get_conference_sessions", (self._data["id"], ), {}
+
+       def _create_child(self, data, id):
+               return SessionNode(self._connection, self, data, id)
+
+
+class SessionNode(ParentNode):
+
+       def __init__(self, connection, parent, data, id):
+               ParentNode.__init__(self, connection, parent, data, id)
+
+       @property
+       def title(self):
+               return self._data["title"]
+
+       def _get_func(self):
+               return "get_conference_talks", (self._data["id"], ), {}
+
+       def _create_child(self, data, id):
+               return TalkNode(self._connection, self, data, id)
+
+
+class TalkNode(LeafNode):
+
+       def __init__(self, connection, parent, data, id):
+               LeafNode.__init__(self, connection, parent, data, id)
+
+       @property
+       def can_navigate(self):
+               return True
+
+       @property
+       def title(self):
+               return self._data["title"]
+
+       @property
+       def subtitle(self):
+               return self._data["speaker"]
+
+       @property
+       def uri(self):
+               return self._data["url"]
+
+
+def walk_ancestors(node):
+       while True:
+               yield node
+               try:
+                       node = node.get_parent()
+               except RuntimeError:
+                       return
+
+
+def common_paths(targetNode, currentNode):
+       targetNodePath = list(walk_ancestors(targetNode))
+       targetNodePath.reverse()
+       currentNodePath = list(walk_ancestors(currentNode))
+       currentNodePath.reverse()
+
+       ancestors = []
+       descendants = []
+
+       for i, (t, c) in enumerate(zip(targetNodePath, currentNodePath)):
+               if t is not c:
+                       return ancestors, None, descendants
+               ancestors.append(t)
+
+       descendants.extend(
+               child
+               for child in targetNodePath[i+1:]
+       )
+
+       return ancestors, currentNode, descendants
index 65947b3..a31d1a2 100644 (file)
@@ -11,6 +11,7 @@ import constants
 import hildonize
 import util.misc as misc_utils
 
+import stream_index
 import banners
 import playcontrol
 import presenter
@@ -100,6 +101,9 @@ class BasicWindow(gobject.GObject):
                else:
                        self._window.unfullscreen()
 
+       def jump_to(self, node):
+               raise NotImplementedError("On %s" % self)
+
        @misc_utils.log_exception(_moduleLogger)
        def _on_destroy(self, *args):
                self._isDestroyed = True
@@ -150,9 +154,7 @@ class BasicWindow(gobject.GObject):
 
        @misc_utils.log_exception(_moduleLogger)
        def _on_jump(self, source, node):
-               _moduleLogger.error("Jump is not implemented")
-               self.emit("jump-to", node)
-               self._window.destroy()
+               raise NotImplementedError("On %s" % self)
 
        @misc_utils.log_exception(_moduleLogger)
        def _on_quit(self, *args):
@@ -170,22 +172,22 @@ class SourceSelector(BasicWindow):
                self._loadingBanner = banners.GenericBanner()
 
                self._radioButton = self._create_button("radio", "Radio")
-               self._radioButton.connect("clicked", self._on_source_selected, RadioWindow, "radio")
+               self._radioButton.connect("clicked", self._on_source_selected, stream_index.SOURCE_RADIO)
                self._radioWrapper = gtk.VBox()
                self._radioWrapper.pack_start(self._radioButton, False, True)
 
                self._conferenceButton = self._create_button("conferences", "Conferences")
-               self._conferenceButton.connect("clicked", self._on_source_selected, ConferencesWindow, "conferences")
+               self._conferenceButton.connect("clicked", self._on_source_selected, stream_index.SOURCE_CONFERENCES)
                self._conferenceWrapper = gtk.VBox()
                self._conferenceWrapper.pack_start(self._conferenceButton, False, True)
 
                self._magazineButton = self._create_button("magazines", "Magazines")
-               #self._magazineButton.connect("clicked", self._on_source_selected)
+               self._magazineButton.connect("clicked", self._on_source_selected, stream_index.SOURCE_MAGAZINES)
                self._magazineWrapper = gtk.VBox()
                self._magazineWrapper.pack_start(self._magazineButton, False, True)
 
                self._scriptureButton = self._create_button("scriptures", "Scriptures")
-               #self._scriptureButton.connect("clicked", self._on_source_selected)
+               self._scriptureButton.connect("clicked", self._on_source_selected, stream_index.SOURCE_SCRIPTURES)
                self._scriptureWrapper = gtk.VBox()
                self._scriptureWrapper.pack_start(self._scriptureButton, False, True)
 
@@ -197,6 +199,7 @@ class SourceSelector(BasicWindow):
                self._buttonLayout.pack_start(self._scriptureWrapper, True, True)
 
                self._playcontrol = playcontrol.NavControl(player, store)
+               self._playcontrol.connect("jump-to", self._on_jump)
 
                self._layout.pack_start(self._loadingBanner.toplevel, False, False)
                self._layout.pack_start(self._buttonLayout, True, True)
@@ -250,15 +253,35 @@ class SourceSelector(BasicWindow):
                self._hide_loading()
                self._errorBanner.push_message(str(exception))
 
-       @misc_utils.log_exception(_moduleLogger)
-       def _on_source_selected(self, widget, Source, nodeName):
-               node = self._index.get_source(nodeName, self._languages[0]["id"])
+       def _window_from_node(self, node):
+               if node.id == stream_index.SOURCE_RADIO:
+                       Source = RadioWindow
+               elif node.id == stream_index.SOURCE_CONFERENCES:
+                       Source = ConferencesWindow
+               elif node.id == stream_index.SOURCE_MAGAZINES:
+                       pass
+               elif node.id == stream_index.SOURCE_SCRIPTURES:
+                       pass
                sourceWindow = Source(self._player, self._store, node)
                sourceWindow.window.set_modal(True)
                sourceWindow.window.set_transient_for(self._window)
                sourceWindow.window.set_default_size(*self._window.get_size())
                sourceWindow.connect("quit", self._on_quit)
+               sourceWindow.connect("jump-to", self._on_jump)
                sourceWindow.show()
+               return sourceWindow
+
+       @misc_utils.log_exception(_moduleLogger)
+       def _on_jump(self, source, node):
+               targetNodePath = list(reversed(list(stream_index.walk_ancestors(node))))
+               ancestor = targetNodePath[0]
+               window = self._window_from_node(ancestor)
+               window.jump_to(node)
+
+       @misc_utils.log_exception(_moduleLogger)
+       def _on_source_selected(self, widget, nodeName):
+               node = self._index.get_source(nodeName, self._languages[0]["id"])
+               self._window_from_node(node)
 
 
 gobject.type_register(SourceSelector)
@@ -334,6 +357,9 @@ class RadioWindow(BasicWindow):
 
                self._refresh()
 
+       def jump_to(self, node):
+               _moduleLogger.info("Only 1 channel, nothing to jump to")
+
        @property
        def _active(self):
                return self._player.node is self._childNode
@@ -556,12 +582,51 @@ class ListWindow(BasicWindow):
                raise NotImplementedError("")
 
        def _get_current_row(self):
+               if self._player.node is None:
+                       return -1
+               ancestors, current, descendants = stream_index.common_paths(self._player.node, self._node)
+               if not descendants:
+                       return -1
+               activeChild = descendants[0]
+               for i, row in enumerate(self._model):
+                       if activeChild is row[0]:
+                               return i
+               else:
+                       return -1
+
+       def jump_to(self, node):
+               ancestors, current, descendants = stream_index.common_paths(node, self._node)
+               if current is None:
+                       raise RuntimeError("Cannot jump to node %s" % node)
+               if not descendants:
+                       _moduleLogger.info("Current node is the target")
+                       return
+               child = descendants[0]
+               window = self._window_from_node(child)
+               window.jump_to(node)
+
+       def _window_from_node(self, node):
                raise NotImplementedError("")
 
        @misc_utils.log_exception(_moduleLogger)
        def _on_row_activated(self, view, path, column):
                raise NotImplementedError("")
 
+       @misc_utils.log_exception(_moduleLogger)
+       def _on_jump(self, source, node):
+               ancestors, current, descendants = stream_index.common_paths(node, self._node)
+               if current is None:
+                       _moduleLogger.info("%s is not the target, moving up" % self._node)
+                       self.emit("jump-to", node)
+                       self._window.destroy()
+                       return
+               if not descendants:
+                       _moduleLogger.info("Current node is the target")
+                       return
+               child = descendants[0]
+               window = self._window_from_node(child)
+               window.jump_to(node)
+
        def _show_loading(self):
                animationPath = self._store.STORE_LOOKUP["loading"]
                animation = self._store.get_pixbuf_animation_from_store(animationPath)
@@ -602,10 +667,6 @@ class ConferencesWindow(ListWindow):
                column.add_attribute(textrenderer, "text", 2)
                yield gobject.TYPE_STRING, column
 
-       def _get_current_row(self):
-               # @todo Not implemented yet
-               return 0
-
        def _refresh(self):
                ListWindow._refresh(self)
                self._node.get_children(
@@ -634,18 +695,22 @@ class ConferencesWindow(ListWindow):
                self._hide_loading()
                self._errorBanner.push_message(str(exception))
 
-       @misc_utils.log_exception(_moduleLogger)
-       def _on_row_activated(self, view, path, column):
-               itr = self._model.get_iter(path)
-               node = self._model.get_value(itr, 0)
-
+       def _window_from_node(self, node):
                sessionsWindow = ConferenceSessionsWindow(self._player, self._store, node)
                sessionsWindow.window.set_modal(True)
                sessionsWindow.window.set_transient_for(self._window)
                sessionsWindow.window.set_default_size(*self._window.get_size())
                sessionsWindow.connect("quit", self._on_quit)
                sessionsWindow.connect("home", self._on_home)
+               sessionsWindow.connect("jump-to", self._on_jump)
                sessionsWindow.show()
+               return sessionsWindow
+
+       @misc_utils.log_exception(_moduleLogger)
+       def _on_row_activated(self, view, path, column):
+               itr = self._model.get_iter(path)
+               node = self._model.get_value(itr, 0)
+               self._window_from_node(node)
 
 
 gobject.type_register(ConferencesWindow)
@@ -667,10 +732,6 @@ class ConferenceSessionsWindow(ListWindow):
                column.add_attribute(textrenderer, "text", 1)
                yield gobject.TYPE_STRING, column
 
-       def _get_current_row(self):
-               # @todo Not implemented yet
-               return 0
-
        def _refresh(self):
                ListWindow._refresh(self)
                self._node.get_children(
@@ -699,18 +760,22 @@ class ConferenceSessionsWindow(ListWindow):
                self._hide_loading()
                self._errorBanner.push_message(str(exception))
 
-       @misc_utils.log_exception(_moduleLogger)
-       def _on_row_activated(self, view, path, column):
-               itr = self._model.get_iter(path)
-               node = self._model.get_value(itr, 0)
-
+       def _window_from_node(self, node):
                sessionsWindow = ConferenceTalksWindow(self._player, self._store, node)
                sessionsWindow.window.set_modal(True)
                sessionsWindow.window.set_transient_for(self._window)
                sessionsWindow.window.set_default_size(*self._window.get_size())
                sessionsWindow.connect("quit", self._on_quit)
                sessionsWindow.connect("home", self._on_home)
+               sessionsWindow.connect("jump-to", self._on_jump)
                sessionsWindow.show()
+               return sessionsWindow
+
+       @misc_utils.log_exception(_moduleLogger)
+       def _on_row_activated(self, view, path, column):
+               itr = self._model.get_iter(path)
+               node = self._model.get_value(itr, 0)
+               self._window_from_node(node)
 
 
 gobject.type_register(ConferenceSessionsWindow)
@@ -732,10 +797,6 @@ class ConferenceTalksWindow(ListWindow):
                column.add_attribute(textrenderer, "text", 1)
                yield gobject.TYPE_STRING, column
 
-       def _get_current_row(self):
-               # @todo Not implemented yet
-               return 0
-
        def _refresh(self):
                ListWindow._refresh(self)
                self._node.get_children(
@@ -764,18 +825,22 @@ class ConferenceTalksWindow(ListWindow):
                self._hide_loading()
                self._errorBanner.push_message(str(exception))
 
-       @misc_utils.log_exception(_moduleLogger)
-       def _on_row_activated(self, view, path, column):
-               itr = self._model.get_iter(path)
-               node = self._model.get_value(itr, 0)
-
+       def _window_from_node(self, node):
                sessionsWindow = ConferenceTalkWindow(self._player, self._store, node)
                sessionsWindow.window.set_modal(True)
                sessionsWindow.window.set_transient_for(self._window)
                sessionsWindow.window.set_default_size(*self._window.get_size())
                sessionsWindow.connect("quit", self._on_quit)
                sessionsWindow.connect("home", self._on_home)
+               sessionsWindow.connect("jump-to", self._on_jump)
                sessionsWindow.show()
+               return sessionsWindow
+
+       @misc_utils.log_exception(_moduleLogger)
+       def _on_row_activated(self, view, path, column):
+               itr = self._model.get_iter(path)
+               node = self._model.get_value(itr, 0)
+               self._window_from_node(node)
 
 
 gobject.type_register(ConferenceTalksWindow)
@@ -817,6 +882,9 @@ class ConferenceTalkWindow(BasicWindow):
                )
                self._set_context(self._player.state)
 
+       def jump_to(self, node):
+               assert self._node is node
+
        @property
        def _active(self):
                return self._player.node is self._node