# GNU Solfege - ear training for GNOME
# Copyright (C) 2005, 2006  Tom Cato Amundsen
#
# This program is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation; either version 2 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software
# Foundation, Inc., 51 Franklin ST, Fifth Floor, Boston, MA  02110-1301  USA

if __name__ == '__main__':
    import testsetup

import unittest
import gtk
import gobject
import pprint
import copy
import sys
import os
import cfg
import gu
import lessonfilegui
from gpath import Path

class Menu(dict):
    def new_from_name(name):
        m = Menu({'name': name, 'children': []})
        return m
    new_from_name = staticmethod(new_from_name)
    def get_name(self):
        return self['name']
    def set_name(self, name):
        self['name'] = name
    name = property(get_name, set_name)
    def get_children(self):
        return self['children']
    def set_children(self, children):
        self['children'] = children
    children = property(get_children, set_children)
    def pformat(self, indent):
        return "%(indent)s{'name': '%(name)s',\n" \
        "%(indent)s 'nametotranslate': _('%(name)s'),\n" \
        "%(indent)s 'children': [\n" \
        "%(children)s ]\n" \
        "%(indent)s}," %  {
            'indent': " " * indent,
            'name': self.name,
            'children': self.pformat_children(indent + 4)
        }
    def pformat_children(self, indent):
        v = []
        for child in self.children:
            if isinstance(child, basestring):
                v.append("%s'%s'," % (" " * indent, child))
            else:
                v.append(child.pformat(indent))
        return "\n".join(v)


class LearningTree:
    def __init__(self, lessonfile_manager):
        self.lessonfile_manager = lessonfile_manager
        self.m_menus = []
        self.m_deps = {}
    def load(self, filename):
        self.m_visibility = 0
        self.m_learning_tree_version = 0
        g = {'_': lambda s: s}
        v = eval(file(filename, 'r').read(), g)
        if len(v) == 2:
            self.m_menus, self.m_deps = v
        elif len(v) == 3:
            self.m_menus, self.m_deps, self.m_visibility = v
        elif len(v) == 4:
            self.m_learning_tree_version, self.m_menus, self.m_deps, self.m_visibility = v
        if self.m_learning_tree_version == 0:
            # Change from old style with one menu to multiple exercise
            # menus on the menu bar. If version == 0, then the tree only
            # have one menu "Exercises"
            self.m_menus = [
              {'name': "_Practise",
               'submenus': self.m_menus},
            ]
            self.m_menus[0]['children'] = [{'name': n['name'], 'children': n['lessons']} for n in self.m_menus[0]['submenus']]
            del self.m_menus[0]['submenus']
        self.m_menus = [Menu(d) for d in self.m_menus]
        for idx, menu in enumerate(self.m_menus):
            self.m_menus[idx].children = [Menu(s) for s in menu.children]
        self.m_learning_tree_version = 2
        junk_id = []
        # FIXME here we are assuming that there are only two levels of menus
        if self.lessonfile_manager:
            # lessonfile_manager is None in some test cases
            for menu in self.m_menus:
                for topic in menu.children:
                    for j in topic.children:
                        if j not in self.lessonfile_manager.m_uiddb:
                            print >> sys.stderr, "Junking the registration of lesson_id '%s'\nfrom learning tree. File not found." % j
                            junk_id.append(j)
        for j in junk_id:
            for menu in self.m_menus:
                for topic in menu.children:
                    if j in topic.children:
                        del topic.children[topic.children.index(j)]
                if j in self.m_deps:
                    del self.m_deps[j]
                for d in self.m_deps:
                    if j in self.m_deps[d]:
                        self.m_deps[d].remove(j)
        self.sort_topics()
        self.calculate_visibilities()
    def save(self, filename):
        ofile = file(filename, 'w')
        print >> ofile, "[ # toplevel list"
        print >> ofile, "  %s, # learning tree file format version" % self.m_learning_tree_version
        print >> ofile, "  [ # start of list of menus"
        for menu in self.m_menus:
            print >> ofile, menu.pformat(4)
        print >> ofile, "  ], # end of list of menus"
        ofile.write("%s,\n%s]" % (pprint.pformat(self.m_deps), self.m_visibility))
        ofile.close()
    def new_menu(self, menuname):
        self.m_menus.append(Menu({'name': menuname, 'children': []}))
    def new_topic(self, menu_idx, topicname):
        assert topicname not in [s.name for s in self.m_menus[menu_idx].children]
        self.m_menus[menu_idx].children.append(Menu.new_from_name(topicname))
    def sort_topics(self):
        """
        Sort the lessons in each topic.
        """
        for menu in self.m_menus:
            for topic in menu.children:
                topic.children.sort(lambda a, b: self.cmp(a, b))
    def move_elem_up(self, path):
        """
        Move elem up. Return True if success.
        Return False if we are the first elem.
        This function only moves the element within the menu.
        """
        if path[-1] == 0:
            return False
        p = path[:-1]
        new_path = path[:-1] + (path[-1],)
        try:
            self.get(p).children[path[-1]], self.get(p).children[path[-1]-1] = \
                self.get(p).children[path[-1]-1], self.get(p).children[path[-1]]
        except IndexError:
            return False
        return True
    def move_elem_to_prev_menu(self, path):
        """
        Move the element pointed to by path to the prev menu.
        Return None if we are on the first menu.
        """
        assert path[-2] > 0
        to_path = list(path[:-1])
        to_path[-1] -= 1
        to_path = tuple(to_path)
        self.get(to_path).children.append(self.get(path))
        del self.get(path[:-1]).children[path[-1]]
        return to_path + (len(self.get(to_path).children)-1,)
    def move_elem_down(self, path):
        """
        Move move the element (submenu or lesson) one step down on the menu
        containing it, and return True if successfull.  Return False and do
        nothing if the element path points to are the last element.
        """
        p = path[:-1]
        try:
            self.get(p).children[path[-1]], \
                    self.get(p).children[path[-1]+1] = \
                    self.get(p).children[path[-1]+1], \
                    self.get(p).children[path[-1]]
        except IndexError:
            return False
        return True
    def move_elem_to_next_menu(self, path):
        """
        Move the element pointed to by path to the next menu.
        Faild miserably if we are on the last menu, because the
        gui checks this right now. Return the path to the new
        position.
        """
        pn = list(path[:-1])
        pn[0] += 1
        pn = tuple(pn)
        self.get(pn).children.insert(0, self.get(path))
        del self.get(path[:-1]).children[path[-1]]
        return pn + (0,)
    def move_lesson_up(self, path):
        """
        Return True if successful, else False.
        The tree is unchanged if we return False.
        """
        if path[-1] == 0:
            # We are the first lesson
            return False
        move_id = self.get(path)
        prev_id = self.get(Path(path).prev())
        if prev_id not in list(self.iter_subdeps(move_id)):
            self.move_elem_up(path)
            return True
        return False
    def move_lesson_down(self, path):
        """
        Return True if successful, else None.
        The tree is unchanged if we return None.
        """
        p = path[:-1]
        i = path[-1]
        move_id = self.get(path)
        next_id = self.get(Path(path).next())
        if move_id not in list(self.iter_subdeps(next_id)):
            try:
                self.get(p).children[i], self.get(p).children[i + 1] = \
                    self.get(p).children[i + 1], self.get(p).children[i]
            except IndexError:
                return False
            return True
        return False
    def add_lesson(self, path, lesson_id):
        """
        Each lesson can only be once in a topic.
        Return True if sucessful, False if not
        """
        menu = self.get(path)
        if lesson_id not in menu.children:
            if not lesson_id in self.m_deps:
                self.m_deps[lesson_id] = []
            menu.children.append(lesson_id)
            menu.children.sort(lambda a, b: self.cmp(a, b))
        else:
            return False
    def delete_lesson(self, path):
        menu = self.get(path[:-1])
        del menu.children[path[-1]]
    def add_dependency(self, lesson_id, dep_id):
        assert dep_id not in self.m_deps[lesson_id]
        self.m_deps[lesson_id].append(dep_id)
        self.sort_topics()
    def delete_dependency(self, lesson_id, id_to_delete):
        i = self.m_deps[lesson_id].index(id_to_delete)
        del self.m_deps[lesson_id][i]
        self.sort_topics()
    def iterate_all_lessons2(self):
        """
        Iterate all lessons that are added to the learning tree.
        Yields the tuple (lesson_ids, path,)
        """
        def do_children(item, path):
            # path == (0,) is the first menu on the menubar
            path = path.child()
            for c in item.children:
                if isinstance(c, Menu):
                    for x in do_children(c, path):
                        yield x
                else:
                    assert isinstance(c, basestring)
                    yield c, path
                path = path.next()
        path = Path((0,))
        for menu in self.m_menus:
            for x in do_children(menu, path):
                yield x
            path = path.next()
    def iterate_all_lessons(self):
        """
        Iterate all lessons that are added to the learning tree.
        Yields lesson_ids
        """
        def do_children(item):
            for c in item.children:
                if isinstance(c, Menu):
                    for x in do_children(c):
                        yield x
                else:
                    assert isinstance(c, basestring)
                    yield c
        for menu in self.m_menus:
            for x in do_children(menu):
                yield x
    def iterate_topics_for_id(self, lesson_id):
        """
        Yield a string with the name of the submenu containing
        the lesson_id.
        """
        def do_menu(menu):
            if lesson_id in menu.children:
                yield menu.name
            for child in menu.children:
                if isinstance(child, Menu):
                    for n in do_menu(child):
                        yield n
        for menu in self.m_menus:
            for n in do_menu(menu):
                yield n
    def iterate_deps_for_id(self, lesson_id):
        """
        Iterate all the direct dependencies for lesson_id.
        It does not iterate the sub-dependencies.
        """
        for dep in self.m_deps[lesson_id]:
            yield dep
    def iterate_possible_deps_for(self, path):
        """
        All lessons, except those on the x-list.
        You get on the x-list if:
        1. is OBJECT
        2. already in the depends list of OBJECT
        3. depend on anything in the x-list
        4. is a dep (of dep)* of OBJ


        Filter out lessons that
        1. is OBJECT
        2. is in depends tree below OBJECT
        3. has OBJECT in its depends tree
        """
        # The lesson_id we are finding possible deps for
        this_id = self.get(path)
        # First, lets make a list of all lessons that currently are in a topic
        used = {}
        for lesson_id in self.iterate_all_lessons():
            used[lesson_id] = True
        # Filter out this_id (point 1 in the list in the docstring)
        del used[this_id]
        def check(lesson_id):
            # Filter out according to #2 and #3 in the docstring
            if lesson_id in list(self.iter_subdeps(this_id)) \
                    or this_id in list(self.iter_subdeps(lesson_id)):
                return False
            return True
        for i in filter(check, used.keys()):
            yield i
    def iter_subdeps(self, lesson_id):
        for n in self.m_deps[lesson_id]:
            yield n
            for nn in self.iter_subdeps(n):
                yield nn
    def is_practisable(self, lesson_id):
        for i in self.iterate_deps_for_id(lesson_id):
            if not self.lessonfile_manager.is_test_passed(i):
                return False
        return True
    def calculate_visibilities(self):
        self.m_visibilities = {}
        v = self.m_deps.keys()
        v.sort(lambda a, b: self.cmp(a, b))
        for i in v:
            if i not in self.lessonfile_manager.m_uiddb:
                continue
            if not self.lessonfile_manager.get(i, 'test'):
                self.m_visibilities[i] = 0
            elif not list(self.iterate_deps_for_id(i)):
                self.m_visibilities[i] = 0
            elif self.is_practisable(i):
                self.m_visibilities[i] = 0
            else:
                self.m_visibilities[i] = max([self.m_visibilities[x] for x in self.m_deps[i]]) + 1
    def cmp(self, id_a, id_b):
        """
        Return -1, 0, 1, like a cmp function.
        """
        deps_a = list(self.iter_subdeps(id_b))
        if id_a in deps_a:
            return -1
        deps_b = list(self.iter_subdeps(id_a))
        if id_b in deps_b:
            return 1
        return cmp(len(deps_b), len(deps_a))
    def get_use_count(self, lesson_id):
        """
        Return an integer telling how many times the lesson lesson_id
        is used as an exercise.
        """
        count = 0
        for i in self.iterate_all_lessons():
            if i == lesson_id:
                count += 1
        return count
    def get_dep_use_count(self, lesson_id):
        """
        Return an integer telling how many lessons that depends on lesson_id.
        """
        count = 0
        for v in self.m_deps.values():
            for i in v:
                if i == lesson_id:
                    count += 1
        return count
    def remove_all_deps_of(self, del_id):
        for v in self.m_deps.values():
            if del_id in v:
                del v[v.index(del_id)]
    def get(self, path):
        """
        Return the element pointed to by path.
        """
        elem = self.m_menus[path[0]]
        for idx in path[1:]:
            elem = elem.children[idx]
        return elem

class LessonFileDialogCommon(gtk.Dialog):
    def __init__(self, app, tree, path):
        gtk.Dialog.__init__(self, _("Select lesson file"), self, buttons=
            (gtk.STOCK_CANCEL, gtk.RESPONSE_CANCEL,))
        self.m_app = app
        self.m_tree = tree
        self.m_app.lessonfile_manager.parse(self.m_app.m_options.debug)
        lessonfilegui.handle_lesson_id_crash(self.m_app.lessonfile_manager)
        self.m_cur_menu_path = path
        self.g_ok_button = self.add_button("gtk-ok", gtk.RESPONSE_OK)
        self.g_ok_button.set_sensitive(False)
        self.set_default_size(800, 500)
        vbox = gtk.VBox()
        self.vbox.pack_start(vbox, False)
        self.g_liststore = gtk.ListStore(gobject.TYPE_STRING,
            gobject.TYPE_STRING, gobject.TYPE_STRING, gobject.TYPE_INT,
            gobject.TYPE_STRING)
        self.update_filelist()
        self.g_treeview = gtk.TreeView(self.g_liststore)
        self.g_treeview.connect('cursor-changed', self.on_cursor_changed)
        renderer = gtk.CellRendererText()
        column = gtk.TreeViewColumn(_("Used"), renderer, text=3)
        column.set_sort_column_id(3)
        column.set_resizable(True)
        self.g_treeview.append_column(column)
        column = gtk.TreeViewColumn(_("Title"), renderer, text=0)
        column.set_resizable(True)
        column.set_sort_column_id(0)
        self.g_treeview.append_column(column)
        column = gtk.TreeViewColumn(_("Filename"), renderer, text=1)
        column.set_resizable(True)
        self.g_treeview.append_column(column)
        def sort_func(store, itera, iterb):
            return cmp(store.get(itera, 1)[0], store.get(iterb, 1)[0])
        self.g_treeview.get_model().set_sort_func(1, sort_func)
        column.set_sort_column_id(1)
        scrolled_window = gtk.ScrolledWindow()
        scrolled_window.add(self.g_treeview)
        self.vbox.pack_start(scrolled_window)
        hbox = gtk.HBox()
        hbox.set_spacing(8)
        hbox.set_border_width(12)
        self.vbox.pack_start(hbox, False)
        hbox.pack_start(gtk.Label(_("Module:")), False)
        self.g_module = gtk.Label()
        hbox.pack_start(self.g_module, False)
        #
        hbox = gtk.HBox()
        hbox.set_spacing(8)
        hbox.set_border_width(12)
        self.vbox.pack_start(hbox, False)
        label = gtk.Label(_("Used by topic(s):"))
        label.set_alignment(0.0, 0.0)
        hbox.pack_start(gtk.Label(_("Used by topic(s):")), False)
        self.g_topics_vbox = gtk.VBox()
        hbox.pack_start(self.g_topics_vbox, False)
        #
        self.show_all()
        self.g_treeview.connect('row-activated', self.on_row_activated)
    def get_selected_lesson_id(self):
        """
        Returns None if no lesson file is selected.
        """
        i = self.g_treeview.get_cursor()[0]
        if i:
            i = i[0]
            iter = self.g_liststore.iter_nth_child(None, i)
            return self.g_liststore.get(iter, 2)[0]
    def on_row_activated(self, treeview, row, treeviewcolumn):
        self.response(gtk.RESPONSE_OK)
    def on_cursor_changed(self, treeview):
        i = self.g_treeview.get_cursor()[0][0]
        iter = self.g_liststore.iter_nth_child(None, i)
        self.g_module.set_text(self.g_liststore.get(iter, 4)[0])
        self.g_topics_vbox.foreach(lambda w: w.destroy())
        for s in self.m_tree.iterate_topics_for_id(
                self.g_liststore.get(iter, 2)):
            label = gtk.Label(s)
            label.set_alignment(0.0, 0.5)
            label.show()
            self.g_topics_vbox.pack_start(label)
        self.g_ok_button.set_sensitive(True)


class SelectLessonFileDialog(LessonFileDialogCommon):
    def __init__(self, app, tree, path):
        LessonFileDialogCommon.__init__(self, app, tree, path)
        g_show_used = gu.nCheckButton('SelectLessonFileDialog',
            'show_used_files', _("_Show files used in other topics"),
            False, self.update_filelist)
        self.vbox.pack_start(g_show_used, False)
        self.vbox.reorder_child(g_show_used, 0)
    def update_filelist(self, w=None):
        self.g_liststore.clear()
        if cfg.get_bool('SelectLessonFileDialog/show_used_files'):
            lesson_id_list = [i for i in list(self.m_app.lessonfile_manager.iterate_lesson_ids()) if i not in self.m_tree.get(self.m_cur_menu_path).children]
        else:
            lesson_id_list = [i for i in list(self.m_app.lessonfile_manager.iterate_lesson_ids()) if self.m_tree.get_use_count(i) == 0]
        for lesson_id in lesson_id_list:
            d = self.m_app.lessonfile_manager.m_uiddb[lesson_id]
            self.g_liststore.append((d['header']['title'], d['filename'], lesson_id, self.m_tree.get_use_count(lesson_id), self.m_app.lessonfile_manager.get(lesson_id, 'module')))


class DepsLessonFileDialog(LessonFileDialogCommon):
    def __init__(self, app, tree, path):
        """
        i is the index from the lesson in the topic in the tree.
        """
        LessonFileDialogCommon.__init__(self, app, tree, path)
        self.update_filelist()
    def update_filelist(self, w=None):
        self.g_liststore.clear()
        for lesson_id in list(self.m_tree.iterate_possible_deps_for(self.m_cur_menu_path)):
            d = self.m_app.lessonfile_manager.m_uiddb[lesson_id]
            self.g_liststore.append((d['header']['title'], d['filename'], lesson_id, self.m_tree.get_use_count(lesson_id), self.m_app.lessonfile_manager.get(lesson_id, 'module')))


class Window(gtk.Window):
    def __init__(self, app):
        gtk.Window.__init__(self)
        self.m_app = app
        self.set_default_size(500, 500)
        self.vbox = gtk.VBox()
        self.add(self.vbox)
        hbox = gtk.HBox()
        self.vbox.pack_start(hbox, False)
        g = gtk.Button(_("Save"))
        g.connect('clicked', self.on_save)
        hbox.pack_start(g, False)
        g = gtk.Button(_("Save as..."))
        g.connect('clicked', self.on_save_as)
        hbox.pack_start(g, False)
        g = gtk.Button(_("Close"))
        g.connect('clicked', self.close_window)
        hbox.pack_start(g, False)
        #######################
        self.g_content_vbox = gtk.VBox()
        self.vbox.pack_start(self.g_content_vbox)
        self.g_content_vbox.set_border_width(12)
        self.g_content_vbox.set_spacing(8)
        buttons_sizegroup = gtk.SizeGroup(gtk.SIZE_GROUP_HORIZONTAL)
        # Visibility
        hbox = gtk.HBox()
        hbox.set_spacing(8)
        l = gtk.Label(_("Default visibility:"))
        hbox.pack_start(l, False)
        self.g_visibility = gtk.Entry()
        self.g_visibility.connect('changed', self.on_visibility_entry_changed)
        hbox.pack_start(self.g_visibility, False)
        self.g_content_vbox.pack_start(hbox, False)
        ##############
        # TreeView with menus and submenus
        hbox = gu.bHBox(self.g_content_vbox)
        hbox.set_spacing(8)
        hbox.show()
        vbox = gu.bVBox(hbox, False, False)
        buttons_sizegroup.add_widget(vbox)
        vbox.show()
        gu.bButton(vbox, _("New toplevel menu"), self.on_new_menu, expand=False)
        self.g_new_submenu = gu.bButton(vbox, _("New submenu"),
                                        self.on_new_topic, expand=False)
        self.g_add_lesson = gtk.Button(_("Add lesson"))
        self.g_add_lesson.connect('clicked', self.on_new_lesson)
        vbox.pack_start(self.g_add_lesson, False)
        self.g_delete_lesson = gtk.Button(_("Delete"))
        self.g_delete_lesson.connect('clicked', self.on_delete)
        vbox.pack_start(self.g_delete_lesson, False)
        self.g_move_topic_up = gtk.Button(_("Move topic up"))
        self.g_move_topic_up.connect('clicked', self.on_move_topic_up)
        vbox.pack_start(self.g_move_topic_up, False)
        self.g_move_topic_down = gtk.Button(_("Move topic down"))
        self.g_move_topic_down.connect('clicked', self.on_move_topic_down)
        vbox.pack_start(self.g_move_topic_down, False)
        self.g_move_lesson_up = gtk.Button(_("Move lesson up"))
        self.g_move_lesson_up.connect('clicked', self.on_move_lesson_up)
        vbox.pack_start(self.g_move_lesson_up, False)
        #
        self.g_move_lesson_down = gtk.Button(_("Move lesson down"))
        self.g_move_lesson_down.connect('clicked', self.on_move_lesson_down)
        vbox.pack_start(self.g_move_lesson_down, False)
        #
        scrolledwindow = gtk.ScrolledWindow()
        scrolledwindow.set_policy(gtk.POLICY_AUTOMATIC, gtk.POLICY_AUTOMATIC)
        scrolledwindow.set_size_request(300, 300)
        scrolledwindow.show()
        self.g_treeview = gtk.TreeView()
        scrolledwindow.add(self.g_treeview)
        hbox.pack_start(scrolledwindow, True, True)
        self.g_treeview.show()
        self.g_treeview.set_rules_hint(True)
        self.g_treeview.connect("cursor-changed", self.treeview_cursor_changed)
        self.__create_columns()
        ###
        self.g_fileinfo_box = gtk.VBox()
        self.g_content_vbox.pack_start(self.g_fileinfo_box, False)
        box = gtk.HBox()
        box.set_spacing(8)
        self.g_fileinfo_box.pack_start(box, False)
        box.pack_start(gtk.Label(_("Lesson filename:")), False)
        self.g_filename = gtk.Label()
        box.pack_start(self.g_filename, False)
        box = gtk.HBox()
        box.set_spacing(8)
        self.g_fileinfo_box.pack_start(box, False)
        box.pack_start(gtk.Label(_("Exercise module:")), False)
        self.g_exercisemodule = gtk.Label()
        box.pack_start(self.g_exercisemodule, False)
        ### Deps
        self.g_deps_box = gtk.HBox()
        self.g_deps_box.set_spacing(8)
        self.g_content_vbox.pack_start(self.g_deps_box, True, True)
        vbox = gtk.VBox() # The box with deps
        buttons_sizegroup.add_widget(vbox)
        #hbox.pack_start(vbox, False)
        self.g_deps_box.pack_start(vbox, False, False)
        self.g_new_dependency = gtk.Button(_("New dependency"))
        self.g_new_dependency.connect('clicked', self.on_add_dep)
        self.g_new_dependency.set_sensitive(False)
        vbox.pack_start(self.g_new_dependency, False)
        self.g_delete_dependency = gtk.Button(_("Delete dependency"))
        self.g_delete_dependency.connect('clicked', self.on_delete_dep)
        self.g_delete_dependency.set_sensitive(False)
        vbox.pack_start(self.g_delete_dependency, False)

        # The box with deps tree and heading
        vbox = gtk.VBox()
        #hbox.pack_start(vbox)
        self.g_deps_box.pack_start(vbox, True, True)
        self.g_deps_heading = gtk.Label()
        self.g_deps_heading.set_alignment(0.0, 0.5)
        self.g_deps_heading.set_line_wrap(True)
        self.g_deps_heading.show()
        vbox.pack_start(self.g_deps_heading, False)
        self.g_deps_scrollwin = gtk.ScrolledWindow()
        self.g_deps_scrollwin.set_policy(gtk.POLICY_AUTOMATIC, gtk.POLICY_AUTOMATIC)
        vbox.pack_start(self.g_deps_scrollwin, False)

        self.g_deps = gtk.TreeView()
        self.g_deps_scrollwin.add(self.g_deps)
        self.g_deps.set_headers_visible(False)
        self.g_deps.connect('cursor-changed', self.on_deps_change)
        #
        renderer = gtk.CellRendererText()
        column = gtk.TreeViewColumn(_("Title"), renderer, text=0)
        self.g_deps.append_column(column)
        self.g_deps_liststore = gtk.ListStore(gobject.TYPE_STRING,
            gobject.TYPE_STRING)
        self.g_deps.set_model(self.g_deps_liststore)
        ###
        self.connect('delete_event', self.on_delete_event)
        self.m_changed = False
        self.show_all()
        self.treeview_cursor_changed()
    def treeview_cursor_changed(self, w=None):
        path = self.g_treeview.get_cursor()[0]
        self.g_new_submenu.set_sensitive(path is not None and len(path) < 2)
        self.g_add_lesson.set_sensitive(path is not None and len(path) == 2)
        self.g_delete_lesson.set_sensitive(path is not None)
        self.g_move_lesson_up.set_sensitive(path is not None and len(path) == 3)
        self.g_move_lesson_down.set_sensitive(path is not None and len(path) == 3)
        self.g_move_topic_up.set_sensitive(path is not None and len(path) == 2)
        self.g_move_topic_down.set_sensitive(path is not None and len(path) == 2)
        if path and isinstance(self.m_tree.get(path), basestring):
            self.g_filename.set_text(self.m_app.lessonfile_manager.get(self.m_tree.get(path), 'filename'))
            self.g_exercisemodule.set_text(self.m_app.lessonfile_manager.get(self.m_tree.get(path), 'module'))
        else:
            self.g_filename.set_text("")
            self.g_exercisemodule.set_text("")
            self.g_deps_liststore.clear()
        self.__selected_lesson_changed()
    def __create_columns(self):
        #
        renderer = gtk.CellRendererText()
        renderer.set_property("xalign", 0.0)
        renderer.set_property('editable', True)
        renderer.connect('edited', self.on_title_edited)
        model = self.g_treeview.get_model()
        column = gtk.TreeViewColumn("Name", renderer, text=0)
        self.g_treeview.append_column(column)
        column = gtk.TreeViewColumn("Type", renderer, text=1)
        self.g_treeview.append_column(column)
    def __create_model(self):
        self.m_model = gtk.TreeStore(gobject.TYPE_STRING, gobject.TYPE_STRING)
        for menu in self.m_tree.m_menus:
            def create_for_children(menu, iter):
                if iter is None:
                    toplevel = True
                else:
                    toplevel = False
                #iter = self.m_model.append(iter)
                if toplevel:
                    type_string = _("Menu")
                else:
                    type_string = _("Submenu")
                iter = self.m_model.append(iter, (menu.name, type_string))
                for child in menu.children:
                    if isinstance(child, Menu):
                        create_for_children(child, iter)
                    else:
                        child_iter = self.m_model.append(iter)
                        self.m_model.set(child_iter, 0, self.m_app.lessonfile_manager.get(child, 'title'), 1, _("Lesson"))
            create_for_children(menu, None)
        self.g_treeview.set_model(self.m_model)
    def on_delete_event(self, *v):
        self.close_window()
    def close_window(self, *v):
        if self.tree_has_changes():
            if gu.dialog_yesno(_("Save changes?"), self):
                self.on_save()
        self.m_app.m_ui.g_learning_tree_editor = None
        self.destroy()
    def on_title_edited(self, renderer, path, txt):
        iter = self.m_model.get_iter_from_string(path)
        p = tuple([int(i) for i in path.split(":")])
        if isinstance(self.m_tree.get(p), basestring):
            return
        self.m_model.set_value(iter, 0, txt)
        self.m_tree.get(p).name = txt
    def on_deps_change(self, treeview_ignore=None):
        self.g_delete_dependency.set_sensitive(True)
    def __selected_lesson_changed(self):
        self.g_deps_liststore.clear()
        path = self.g_treeview.get_cursor()[0]
        self.g_delete_dependency.set_sensitive(False)
        if not path:
            return
        selected = self.m_tree.get(path)
        if isinstance(selected, basestring):
            for x in self.m_tree.iterate_deps_for_id(selected):
                self.g_deps_liststore.append((
                    self.m_app.lessonfile_manager.get(x, 'title'), x))
            self.g_new_dependency.set_sensitive(True)
        else:
            self.g_deps_heading.set_text("")
            self.g_new_dependency.set_sensitive(False)
            return
        lesson_title = self.m_app.lessonfile_manager.get(selected, 'title')
        if list(self.m_tree.iterate_deps_for_id(selected)):
            self.g_deps.show()
            self.g_deps_heading.set_text("<b>%s</b>" % _("Dependencies of the lesson '%s'") % lesson_title)
        else:
            self.g_deps.hide()
            self.g_deps_heading.set_text("<b>%s</b>" % _("No dependencies for the lesson '%s'.") % lesson_title)
        self.g_deps_heading.set_use_markup(True)
        self.g_fileinfo_box.show()
        self.g_deps_box.show()
        self.g_delete_lesson.set_sensitive(True)
    def update_gui(self):
        self.__create_model()
        self.set_title(self.m_filename)
    def on_visibility_entry_changed(self, *v):
        try:
            if self.g_visibility.get_text():
                self.m_tree.m_visibility = int(self.g_visibility.get_text())
        except ValueError, e:
            print >> sys.stderr, e
            print >> sys.stderr, "Setting learning tree visibility to 0."
            self.m_tree.m_visibility = 0
    def _do_save(self, filename):
        """
        Save the tree to 'filename', and return True if sucessful.
        Display message describing the problem and return False is failed.
        """
        try:
            self.m_tree.save(filename)
        except IOError, e:
            gu.dialog_ok(_("An error occurred while saving the file:\n%s") % e,
                    self)
            return False
        return True
    def on_save(self, *v):
        if self._do_save(self.m_filename):
            self.m_app.m_ui.on_learning_tree_changed()
            self._m_orig_deps = copy.deepcopy(self.m_tree.m_deps)
            self._m_orig_menus = copy.deepcopy(self.m_tree.m_menus)
            self._m_orig_visibility = self.m_tree.m_visibility
    def tree_has_changes(self):
        """
        Return True if the tree has unsaved changes.
        """
        return not (self.m_tree.m_menus == self._m_orig_menus
                and self.m_tree.m_deps == self._m_orig_deps
                and self.m_tree.m_visibility == self._m_orig_visibility)
    def on_save_as(self, *v):
        dialog = gtk.FileChooserDialog(_("Save as..."), self,
                                   gtk.FILE_CHOOSER_ACTION_SAVE,
                                   (gtk.STOCK_CANCEL, gtk.RESPONSE_CANCEL,
                                    gtk.STOCK_SAVE_AS, gtk.RESPONSE_ACCEPT))
        dialog.set_default_response(gtk.RESPONSE_ACCEPT)
        run = dialog.run()
        filename = dialog.get_filename()
        dialog.destroy()
        if run == gtk.RESPONSE_ACCEPT:
            if self._do_save(filename):
                self.m_filename = filename
                self.set_title(os.path.basename(self.m_filename))
    def on_new_menu(self, *v):
        dlg = gtk.Dialog(_("Create new menu"), self, buttons=
            (gtk.STOCK_CANCEL, gtk.RESPONSE_CANCEL,
            gtk.STOCK_OK, gtk.RESPONSE_OK))
        dlg.set_default_response(gtk.RESPONSE_OK)
        vbox = gtk.VBox()
        dlg.vbox.pack_start(vbox)
        vbox.set_border_width(18)
        self.g_heading = gtk.Label("<b>%s</b>" % _("Create new menu"))
        self.g_heading.set_alignment(0.0, 0.5)
        vbox.pack_start(self.g_heading, False)
        hbox = gtk.HBox()
        hbox.set_spacing(12)
        vbox.pack_start(hbox, False)
        self.g_heading.set_use_markup(True)
        self.g_message = gtk.Label(_("Menu name:"))
        self.g_entry = gtk.Entry()
        self.g_entry.set_activates_default(True)
        hbox.pack_start(self.g_message, False)
        hbox.pack_start(self.g_entry, False)
        dlg.show_all()
        ret = dlg.run()
        dlg.destroy()
        if ret == gtk.RESPONSE_OK:
            n = self.g_entry.get_text()
            self.m_tree.new_menu(n)
            self.m_model.append(None, (n, _("Menu")))
    def on_new_topic(self, *v):
        menu_idx = self.g_treeview.get_cursor()[0][0]
        dlg = gtk.Dialog(_("New topic"), self, buttons=
            (gtk.STOCK_CANCEL, gtk.RESPONSE_CANCEL,
            gtk.STOCK_OK, gtk.RESPONSE_OK))
        dlg.set_default_response(gtk.RESPONSE_OK)
        vbox = gtk.VBox()
        dlg.vbox.pack_start(vbox)
        vbox.set_border_width(18)
        self.g_heading = gtk.Label("<b>%s</b>" % _("Create new topic"))
        self.g_heading.set_alignment(0.0, 0.5)
        vbox.pack_start(self.g_heading, False)
        hbox = gtk.HBox()
        hbox.set_spacing(12)
        vbox.pack_start(hbox, False)
        self.g_heading.set_use_markup(True)
        self.g_message = gtk.Label(_("Topic name:"))
        self.g_entry = gtk.Entry()
        self.g_entry.set_activates_default(True)
        hbox.pack_start(self.g_message, False)
        hbox.pack_start(self.g_entry, False)
        dlg.show_all()
        ret = dlg.run()
        dlg.destroy()
        if ret == gtk.RESPONSE_OK:
            n = self.g_entry.get_text()
            if n:
                self.m_tree.new_topic(menu_idx, n)
                iter = self.m_model.iter_nth_child(None, menu_idx)
                new_iter = self.m_model.append(iter, (n, _("Submenu")))
                self.g_treeview.expand_to_path(self.m_model.get_path(new_iter))
                self.g_treeview.set_cursor(self.m_model.get_path(new_iter))
    def on_move_topic_up(self, *v):
        path = self.g_treeview.get_cursor()[0]
        if path[-1] > 0:
            iter = self.m_model.get_iter(path)
            iter_b = self.m_model.get_iter(path[:-1] + (path[-1]-1,))
            self.m_model.swap(iter, iter_b)
            self.m_tree.move_elem_up(path)
            self.g_treeview.scroll_to_cell(Path(path).prev())
        elif path[-2] > 0:
            path_new = self.m_tree.move_elem_to_prev_menu(path)
            self.m_model.clear()
            self.__create_model()
            self.g_treeview.expand_to_path(path[:-1])
            self.g_treeview.expand_to_path(path_new[:-1])
            self.g_treeview.set_cursor(path_new)
            self.g_treeview.scroll_to_cell(path_new)
    def on_move_topic_down(self, *v):
        path = self.g_treeview.get_cursor()[0]
        iter = self.m_model.get_iter(path)
        iter_b = self.m_model.iter_next(iter)
        if iter_b:
            # Here we move a menu within the parent menu.
            self.m_model.swap(iter, iter_b)
            if not self.m_tree.move_elem_down(path):
                raise Exception("Completely broken in on_move_topic_down. Should never get here.")
            self.g_treeview.scroll_to_cell(Path(path).next())
        else:
            # Get an iter for the next menu. If iter_b becomes None, then
            # we are the last menu.
            iter_b = self.m_model.iter_parent(iter)
            iter_b = self.m_model.iter_next(iter_b)
            if not iter_b:
                return
                if isinstance(self.m_tree.get(p), basestring):
                    return
            path_new = self.m_tree.move_elem_to_next_menu(path)
            self.m_model.clear()
            self.__create_model()
            self.g_treeview.expand_to_path(path)
            self.g_treeview.expand_to_path(path_new[:-1])
            self.g_treeview.set_cursor(path_new)
            gobject.idle_add(self.g_treeview.scroll_to_cell, path_new)
    def on_delete_dep(self, *v):
        path = self.g_treeview.get_cursor()[0]
        dep_path = self.g_deps.get_cursor()[0]
        if dep_path:
            dep_iter = self.g_deps_liststore.get_iter(dep_path)
            dep_id = self.g_deps_liststore.get(dep_iter, 1)[0]
            lesson_id = self.m_tree.get(path)
            self.m_tree.delete_dependency(lesson_id, dep_id)
            # When we remove deps, the topic is resorted, so we have to
            # recreate the model
            self.m_model.clear()
            self.__create_model()
            self.g_treeview.expand_to_path(path)
            path = Path(path)
            path = path.first()
            while self.m_tree.get(path) != lesson_id:
                path = path.next()
            self.g_treeview.set_cursor(path)
            gobject.idle_add(self.g_treeview.scroll_to_cell, path, None, True, 0.5)
            self.__selected_lesson_changed()
    def on_add_dep(self, *v):
        path = self.g_treeview.get_cursor()[0]
        if path:
            dlg = DepsLessonFileDialog(self.m_app, self.m_tree, path)
            ret = dlg.run()
            if ret == gtk.RESPONSE_OK:
                new_dep_id = dlg.get_selected_lesson_id()
                if new_dep_id:
                    added_to_id = self.m_tree.get(path)
                    self.m_tree.add_dependency(added_to_id, new_dep_id)
                    self.m_model.clear()
                    self.__create_model()
                    self.g_treeview.expand_to_path(path)
                    path = Path(path)
                    path = path.first()
                    while self.m_tree.get(path) != added_to_id:
                        path = path.next()
                    self.g_treeview.set_cursor(path)
                    self.g_treeview.scroll_to_cell(path)
            dlg.destroy()
    def on_new_lesson(self, *v):
        path = self.g_treeview.get_cursor()[0]
        dlg = SelectLessonFileDialog(self.m_app, self.m_tree, path)
        dlg.run()
        lesson_id = dlg.get_selected_lesson_id()
        if lesson_id:
            self.m_tree.add_lesson(path, lesson_id)
            iter = self.m_model.get_iter(path)
            self.m_model.append(iter, (self.m_app.lessonfile_manager.get(lesson_id, 'title'), _("Lesson")))
            self._recreate_tree(lesson_id)
        dlg.destroy()
    def _recreate_tree(self, focus_id):
        self.m_model.clear()
        self.__create_model()
        for lesson_id, path in self.m_tree.iterate_all_lessons2():
            if lesson_id == focus_id:
                break
        self.g_treeview.expand_to_path(path)
        self.g_treeview.set_cursor(path)
    def delete_menu(self, path):
        """
        FIXME Deleting a menu will not delete the dependencies between
        the exerises. This mean that the dependencies will exist when you
        add the lessons later.
        """
        if len(path) == 1:
            del self.m_tree.m_menus[path[0]]
        else:
            v = self.m_tree.get(path[:-1])
            del v['children'][path[-1]]
        self.m_model.clear()
        self.__create_model()
        if len(path) > 1:
            self.g_treeview.expand_to_path(path[:-1])
    def on_delete(self, *v):
        path = self.g_treeview.get_cursor()[0]
        if path:
            del_elem = self.m_tree.get(path)
            if isinstance(del_elem, Menu):
                self.delete_menu(path)
                return
            del_id = del_elem
            do_del = True
            if self.m_tree.get_use_count(del_id) == 1:
                if self.m_tree.get_dep_use_count(del_id) > 0:
                    do_del = gu.dialog_yesno(_("Lessons depend on this lesson. Delete anyway, and update the dependency list for lessons that depend on this lesson?"), self)
                    if do_del:
                        self.m_tree.remove_all_deps_of(del_id)
            if do_del:
                self.m_tree.delete_lesson(path)
                self.m_model.remove(self.m_model.get_iter(path))
                self.g_treeview.set_cursor(path)
    def on_move_lesson_up(self, *v):
        path = self.g_treeview.get_cursor()[0]
        if not path:
            return
        if self.m_tree.move_lesson_up(path):
            itera = self.m_model.get_iter(path)
            iterb = self.m_model.get_iter(Path(path).prev())
            self.m_model.swap(itera, iterb)
            self.g_treeview.scroll_to_cell(Path(path).prev())
    def on_move_lesson_down(self, *v):
        path = self.g_treeview.get_cursor()[0]
        if not path:
            return
        if self.m_tree.move_lesson_down(path):
            next_path = Path(path).next()
            iter = self.m_model.get_iter(path)
            self.m_model.swap(iter, self.m_model.get_iter(next_path))
            self.g_treeview.scroll_to_cell(next_path)
    def load_file(self, filename):
        self.m_tree = LearningTree(self.m_app.lessonfile_manager)
        # We use _m_orig_tree just to see if things has changed.
        self.m_filename = filename
        self.m_tree.load(filename)
        self.g_visibility.set_text(str(self.m_tree.m_visibility))
        self._m_orig_deps = copy.deepcopy(self.m_tree.m_deps)
        self._m_orig_menus = copy.deepcopy(self.m_tree.m_menus)
        self._m_orig_visibility = self.m_tree.m_visibility
        self.update_gui()
        #FIXME uncomment when complete
        #if self.g_topics_liststore.get_iter_first():
        #    self.g_topics.set_cursor("0", None)

class TestLearningTree(unittest.TestCase):
    def testCreate(self):
        tree = LearningTree(None)
        tree.new_menu("Menu 1")
    def create_menu(self):
        tree = LearningTree(None)
        tree.new_menu("Menu 1")
        tree.new_topic(0, "Submenu 0")
        tree.new_topic(0, "Submenu 1")
        tree.new_menu("Menu 2")
        tree.new_topic(1, "Submenu 1-0")
        tree.new_topic(1, "Submenu 1-1")
        return tree
    def test_move_elem_up(self):
        tree = self.create_menu()
        self.assertEqual(tree.get((0, 0))['name'], 'Submenu 0')
        self.assert_(tree.move_elem_up((0, 1)))
        self.assertEqual(tree.get((0, 0))['name'], 'Submenu 1')
        self.assertEqual(tree.get((0, 1))['name'], 'Submenu 0')
        self.assertEqual(tree.move_elem_up((3, 3)), False, 
            'Should fail. Invalid path given to function.')
        self.assertEqual(tree.move_elem_up((1, 0)), False,
            'Should fail. Trying to move the first element up.')
        self.assertEqual(tree.move_elem_up((1,)), False,
            'Should fail becuase move_elem_up cannot move toplevel menus')
    def test_move_elem_to_prev_menu(self):
        tree = self.create_menu()
        self.assertEqual(tree.move_elem_to_prev_menu((1, 0)), (0, 2))
        self.assertEqual(tree.get((0, 2))['name'], 'Submenu 1-0')
    def test_iterate_all_lessons(self):
        tree = LearningTree(None)
        tree.new_menu("Menu 1")
        tree.new_topic(0, "Submenu 0")
        tree.add_lesson((0, 0), "id1")
        tree.add_lesson((0, 0), "id2")
        tree.add_lesson((0, 0), "id3")
        v = list(tree.iterate_all_lessons())
        self.assertEqual(v, ['id1', 'id2', 'id3'])
    def test_iterate_all_lessons2(self):
        tree = LearningTree(None)
        tree.new_menu("Menu 1")
        tree.new_topic(0, "Submenu 0")
        tree.add_lesson((0, 0), "id1")
        tree.add_lesson((0, 0), "id2")
        tree.add_lesson((0, 0), "id3")
        tree.new_topic(0, "Submenu 1")
        tree.add_lesson((0, 1), "id11")
        tree.add_lesson((0, 1), "id12")
        tree.add_lesson((0, 1), "id13")
        v = list(tree.iterate_all_lessons2())
        self.assertEqual(v, [('id1', (0, 0, 0)), ('id2', (0, 0, 1)), ('id3', (0, 0, 2)), ('id11', (0, 1, 0)), ('id12', (0, 1, 1)), ('id13', (0, 1, 2))])



if __name__ == '__main__':
    unittest.main()
    #import lessonfile
    #manager = lessonfile.LessonFileManager()
    #tree = LearningTree(manager)
    #tree.load("learningtree.txt")
    #tree.save("ut.txt")
    #for n in tree.iterate_topics_for_id("solfege.org-all-harmonic-intervals"):
    #    print n, type(n)

