# GNU Solfege - free ear training software
# Copyright (C) 2005, 2006, 2007 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
import gtk
import gobject
import pprint
import copy
import sys
import os
import cfg
import gu
import lessonfilegui
from gpath import Path
import filesystem
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 = {}
self.m_modified = False
def load(self, filename):
self.m_visibility = 0
self.m_learning_tree_version = 0
g = {'_': lambda s: s}
v = eval(open(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()
self.m_modified = False
def save(self, filename):
ofile = open(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()
self.m_modified = False
def new_menu(self, menuname):
self.m_menus.append(Menu({'name': menuname, 'children': []}))
self.m_modified = True
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))
self.m_modified = True
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
self.m_modified = True
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]]
self.m_modified = True
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
self.m_modified = True
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]]
self.m_modified = True
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)
self.m_modified = True
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)
try:
next_id = self.get(Path(path).next())
except IndexError:
return
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
self.m_modified = True
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
self.m_modified = True
return True
def delete_lesson(self, path):
menu = self.get(path[:-1])
del menu.children[path[-1]]
self.m_modified = True
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()
self.m_modified = True
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()
self.m_modified = True
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)]
self.m_modified = True
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)[0]):
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(stock='gtk-new')
g.connect('clicked', self.on_new)
hbox.pack_start(g, False)
g = gtk.Button(stock='gtk-save')
g.connect('clicked', self.on_save)
hbox.pack_start(g, False)
g = gtk.Button(stock='gtk-save-as')
g.connect('clicked', self.on_save_as)
hbox.pack_start(g, False)
g = gtk.Button(stock='gtk-close')
g.connect('clicked', self.close_window)
hbox.pack_start(g, False)
hbox = gtk.HBox()
self.vbox.pack_start(hbox, False)
gu.bLabel(hbox, _("Learning tree:"), False, False)
self.g_trees_liststore = gtk.ListStore(gobject.TYPE_STRING)
self.g_trees = gtk.ComboBox(self.g_trees_liststore)
cell = gtk.CellRendererText()
self.g_trees.pack_start(cell, True)
self.g_trees.add_attribute(cell, 'text', 0)
self.g_trees.connect('changed', self.on_learning_tree_combo_changed)
self.fill_trees_combo()
hbox.pack_start(self.g_trees)
self.g_trees.show()
#######################
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_modified = False
self.show_all()
self.treeview_cursor_changed()
self.__load_file2_lock = 0
def fill_trees_combo(self):
self.m_trees = [('solfege', 'learningtree.txt', os.path.join(os.getcwd(), 'learningtree.txt'))]
if self.m_app.m_options.debug:
self.m_trees.append(('solfege', 'debugtree.txt', os.path.join(os.getcwd(), 'debugtree.txt')))
try:
v = os.listdir(os.path.join(filesystem.app_data(), "learningtrees"))
except OSError:
v = []
for fn in v:
self.m_trees.append(('user', fn, os.path.join(filesystem.app_data(), "learningtrees", fn)))
self.g_trees_liststore.clear()
for t, fn, txt in self.m_trees:
self.g_trees_liststore.append((txt,))
def set_trees_combo(self, place, filename):
# Set the combo to the correct learning tree.
self.__load_file2_lock += 1
idx = 0
while idx < len(self.m_trees):
if self.m_trees[idx][0] == place \
and self.m_trees[idx][1] == filename:
self.g_trees.set_active(idx)
idx += 1
self.__load_file2_lock -= 1
def on_learning_tree_combo_changed(self, combobox):
t, fn, txt = self.m_trees[combobox.get_active()]
if self.m_tree.m_modified:
if gu.dialog_yesno(_("The file is not saved. Save before changing?"), self):
self._do_save(self.m_filename)
if not self.__load_file2_lock:
self.load_file2(t, fn)
self.treeview_cursor_changed()
cfg.set_list('app/learningtree', [t, fn])
self.m_app.m_ui.on_learning_tree_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_new(self, widget):
if self.m_tree.m_modified:
if gu.dialog_yesno(_("The file is not saved. Save before changing?"), self):
self._do_save(self.m_filename)
dlg = gtk.Dialog(_("Create new learning tree"), 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("%s" % _("Create new learning tree"))
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)
donot_str = _("Do not enter any directory names, only the filename.")
self.g_filename_description = gtk.Label(donot_str)
self.g_message = gtk.Label(_("File 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()
vbox.pack_start(self.g_filename_description, False)
while 1:
ret = dlg.run()
if ret == gtk.RESPONSE_OK:
path, filename = os.path.split(self.g_entry.get_text())
if path or (not filename):
self.g_filename_description.set_text(donot_str)
self.g_filename_description.show()
elif os.path.exists(os.path.join(filesystem.app_data(), 'learningtrees', filename)):
self.g_filename_description.set_text(_("The file already exists."))
self.g_filename_description.show()
else:
try:
if not os.path.exists(os.path.join(filesystem.app_data(), 'learningtrees')):
os.makedirs(os.path.join(filesystem.app_data(), 'learningtrees'))
f = open(os.path.join(filesystem.app_data(), 'learningtrees', filename), 'w')
f.write("""
[ # toplevel list
2, # learning tree file format version
[ # start of list of menus
], # end of list of menus
{},
100]
""")
f.close()
self.fill_trees_combo()
self.load_file2('user', filename)
cfg.set_list('app/learningtree', ['user', filename])
self.m_app.m_ui.on_learning_tree_changed()
except IOError, e:
gu.dialog_ok(_("An error occurred while saving the file:\n%s") % e, self)
break
if ret == gtk.RESPONSE_CANCEL:
break
dlg.destroy()
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("%s" % _("Dependencies of the lesson '%s'") % lesson_title)
else:
self.g_deps.hide()
self.g_deps_heading.set_text("%s" % _("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("%s" % _("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()
self.g_explain = gtk.Label()
vbox.pack_start(self.g_explain, False)
while 1:
ret = dlg.run()
if ret == gtk.RESPONSE_OK:
n = self.g_entry.get_text()
if n in [s['name'] for s in self.m_tree.m_menus]:
self.g_explain.set_text(_("The menu name is already used."))
self.g_explain.show()
elif not n:
self.g_explain.set_text(_("An empty string is not allowed."))
self.g_explain.show()
else:
self.m_tree.new_menu(n)
self.m_model.append(None, (n, _("Menu")))
break
elif ret == gtk.RESPONSE_CANCEL:
break
dlg.destroy()
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("%s" % _("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()
self.g_explain = gtk.Label()
vbox.pack_start(self.g_explain, False)
while 1:
ret = dlg.run()
if ret == gtk.RESPONSE_OK:
n = self.g_entry.get_text()
if not n:
self.g_explain.set_text(_("An empty string is not allowed."))
self.g_explain.show()
elif n in [c['name'] for c in self.m_tree.m_menus[menu_idx]['children']]:
self.g_explain.set_text(_("The menu name is already used."))
self.g_explain.show()
else:
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))
break
elif ret == gtk.RESPONSE_CANCEL:
break
dlg.destroy()
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)
self.treeview_cursor_changed()
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_file2(self, place, filename):
"""
place is 'solfege' or 'user'
filename is a filename, with no directory part.
This function will also make sure the g_trees combo is set.
"""
if place == 'solfege':
self._load_file(filename)
else:
assert place == 'user'
self._load_file(os.path.join(filesystem.app_data(), 'learningtrees', filename))
self.set_trees_combo(place, filename)
#
if place == 'solfege':
if not os.access(filename, os.W_OK):
m = gtk.MessageDialog(self, gtk.DIALOG_MODAL, gtk.MESSAGE_INFO,
gtk.BUTTONS_CLOSE, _("The default learning tree is write protected in your install. This is normal. If you want to edit a learning tree, you have to select one of the trees stored in .solfege/learningtrees in your home directory."))
m.run()
m.destroy()
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)