# vim: set fileencoding=utf-8 :
# GNU Solfege - free ear training software
# Copyright (C) 2009  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 3 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, see <http://www.gnu.org/licenses/>.


from __future__ import absolute_import

import logging
import os
import StringIO
import subprocess
import sys
import time
import weakref

import gobject
import gtk

from solfege.esel import SearchView

if __name__ == '__main__':
    from solfege import i18n
    i18n.setup(".", "C")
    import solfege.statistics
    solfege.db = solfege.statistics.DB()

import solfege
from solfege import cfg
from solfege import filesystem
from solfege import gu
from solfege import frontpage as pd
from solfege import lessonfile
from solfege import lessonfilegui
from solfege import osutils

class LessonFilePreviewWidget(gtk.VBox):
    def __init__(self, model):
        gtk.VBox.__init__(self)
        self.m_model = model
        self.set_size_request(200, 200)
        l = gtk.Label()
        l.set_alignment(0.0, 0.5)
        l.set_markup("<b>Title:</b>")
        self.pack_start(l, False)
        self.g_title = gtk.Label()
        self.g_title.set_alignment(0.0, 0.5)
        self.pack_start(self.g_title, False)
        l = gtk.Label()
        l.set_alignment(0.0, 0.5)
        l.set_markup("<b>Module:</b>")
        self.pack_start(l, False)
        self.g_module = gtk.Label()
        self.g_module.set_alignment(0.0, 0.5)
        self.pack_start(self.g_module, False)
        l = gtk.Label()
        l.set_alignment(0.0, 0.5)
        l.set_markup("<b>Used in topcis:</b>")
        self.pack_start(l, False)
        self.g_topic_box = gtk.VBox()
        self.pack_start(self.g_topic_box, False)
        self.show_all()
    def update(self, dlg):
        fn = dlg.get_preview_filename()
        if fn:
            fn = gu.decode_filename(fn)
            for child in self.g_topic_box.get_children():
                child.destroy()
            fn = lessonfile.mk_uri(fn)
            try:
                self.set_sensitive(True)
                self.g_title.set_text(lessonfile.infocache.get(fn, 'title'))
                self.g_module.set_text(lessonfile.infocache.get(fn, 'module'))
                self.g_ok_button.set_sensitive(True)
                for x in self.m_model.iterate_topics_for_file(fn):
                    l = gtk.Label(x)
                    l.set_alignment(0.0, 0.5)
                    self.g_topic_box.pack_start(l, False)
                if not self.g_topic_box.get_children():
                    l = gtk.Label(u"-")
                    l.set_alignment(0.0, 0.5)
                    self.g_topic_box.pack_start(l, False)
            except (lessonfile.InfoCache.FileNotFound,
                    lessonfile.InfoCache.FileNotLessonfile), e:
                self.g_title.set_text(u'')
                self.g_module.set_text(u'')
                self.g_ok_button.set_sensitive(False)
                self.set_sensitive(False)
        self.show_all()
        return True

class SelectLessonFileDialog(gtk.FileChooserDialog):
    def __init__(self, parent):
        gtk.FileChooserDialog.__init__(self, _("Select lesson file"),
            parent=parent,
            buttons=(gtk.STOCK_CANCEL, gtk.RESPONSE_CANCEL,))
        self.set_select_multiple(True)
        pv = LessonFilePreviewWidget(parent.m_model)
        pv.g_ok_button = self.add_button("gtk-ok", gtk.RESPONSE_OK)
        pv.g_ok_button.set_sensitive(False)
        pv.show()
        self.set_preview_widget(pv)
        self.connect('selection-changed', pv.update)


class SelectLessonfileBySearchDialog(gtk.Dialog):
    def __init__(self):
        gtk.Dialog.__init__(self, buttons=(gtk.STOCK_CLOSE, gtk.RESPONSE_ACCEPT))
        view = SearchView(_('Search for exercises. Each exercise you click will be added to the section of the front page.'),
            fields=['link-with-filename-tooltip', 'module'])
        view.on_link_clicked = self.on_link_clicked
        self.vbox.pack_start(view)
        self.show_all()
    def on_link_clicked(self, widget, filename):
        self.m_filename = filename
        self.response(gtk.RESPONSE_OK)


def editor_of(obj):
    """
    Return the toplevel page, the one that is a Editor object.
    """
    p = obj
    while not isinstance(p, Editor):
        p = p.m_parent
    return p

def parent_page(obj):
    """
    Return the parent page of obj. Return None if this is the toplevel page.
    """
    p = obj
    while True:
        p = p.m_parent
        if isinstance(p, Page):
            return p
        if p is None:
            return None

class Section(gtk.VBox):
    """
    A section consists of a heading and a list of links.
    self.g_link_box is a vbox that contains the links.
    """
    def __init__(self, model, parent):
        gtk.VBox.__init__(self)
        self.m_model = model
        self.m_parent = parent
        assert isinstance(model, pd.LinkList)
        hbox = gtk.HBox()
        hbox.set_spacing(6)
        self.pack_start(hbox, False)
        # This is displayed and used when we edit the heading
        self.g_heading_entry = gtk.Entry()
        self.g_heading_entry.set_no_show_all(True)
        hbox.pack_start(self.g_heading_entry)
        self.g_heading = gtk.Label()
        self.g_heading.set_alignment(0.0, 0.5)
        # FIXME escape m_name
        self.g_heading.set_markup("<b>%s</b>" % model.m_name)
        hbox.pack_start(self.g_heading, False)
        #
        button_hbox = gtk.HBox()
        button_hbox.set_spacing(0)
        hbox.pack_start(button_hbox, False)
        im = gtk.Image()
        im.set_from_stock(gtk.STOCK_EDIT, gtk.ICON_SIZE_MENU)
        button = gtk.Button()
        button.add(im)
        button.connect('clicked', self.on_edit_heading)
        button_hbox.pack_start(button, False)
        #
        im = gtk.Image()
        im.set_from_stock(gtk.STOCK_ADD, gtk.ICON_SIZE_MENU)
        button = gtk.Button()
        button.add(im)
        button.connect('button-release-event', self.on_add)
        button_hbox.pack_start(button, False)
        #
        im = gtk.Image()
        im.set_from_stock(gtk.STOCK_REMOVE, gtk.ICON_SIZE_MENU)
        button = gtk.Button()
        button.add(im)
        button.connect('button-release-event', self.on_remove)
        button_hbox.pack_start(button, False)
        #
        im = gtk.Image()
        im.set_from_stock(gtk.STOCK_CUT, gtk.ICON_SIZE_MENU)
        b = gtk.Button()
        b.add(im)
        b.connect('clicked', self.on_cut)
        button_hbox.pack_start(b, False)
        #
        im = gtk.Image()
        im.set_from_stock(gtk.STOCK_PASTE, gtk.ICON_SIZE_MENU)
        b = gtk.Button()
        b.add(im)
        b.connect('clicked', self.on_paste, -1)
        Editor.clipboard.register_paste_button(b, (pd.LinkList, pd.Page, unicode))
        button_hbox.pack_start(b, False)
        #
        im = gtk.Image()
        im.set_from_stock(gtk.STOCK_GO_DOWN, gtk.ICON_SIZE_MENU)
        self.g_move_down_btn = gtk.Button()
        self.g_move_down_btn.add(im)
        self.g_move_down_btn.connect('clicked',
            self.m_parent.move_section_down, self)
        button_hbox.pack_start(self.g_move_down_btn, False)
        #
        im = gtk.Image()
        im.set_from_stock(gtk.STOCK_GO_UP, gtk.ICON_SIZE_MENU)
        self.g_move_up_btn = gtk.Button()
        self.g_move_up_btn.add(im)
        self.g_move_up_btn.connect('clicked',
            self.m_parent.move_section_up, self)
        button_hbox.pack_start(self.g_move_up_btn, False)
        #
        im = gtk.Image()
        im.set_from_stock(gtk.STOCK_GO_BACK, gtk.ICON_SIZE_MENU)
        self.g_move_left_btn = gtk.Button()
        self.g_move_left_btn.add(im)
        self.g_move_left_btn.connect('clicked',
            parent.m_parent.on_move_section_left, self)
        button_hbox.pack_start(self.g_move_left_btn, False)
        #
        im = gtk.Image()
        im.set_from_stock(gtk.STOCK_GO_FORWARD, gtk.ICON_SIZE_MENU)
        self.g_move_right_btn = gtk.Button()
        self.g_move_right_btn.add(im)
        self.g_move_right_btn.connect('clicked',
            parent.m_parent.on_move_section_right, self)
        button_hbox.pack_start(self.g_move_right_btn, False)
        #
        self.g_link_box = gtk.VBox()
        self.pack_start(self.g_link_box, False)
        for link in self.m_model:
            self.g_link_box.pack_start(self.create_linkrow(link))
        # The button to click to add a new link
        hbox = gtk.HBox()
        self.pack_start(hbox)
    def on_edit_heading(self, btn):
        self.g_heading_entry.set_text(self.m_model.m_name)
        self.g_heading_entry.show()
        self.g_heading.hide()
        self.g_heading_entry.grab_focus()
        def finish_edit(entry):
            self.g_heading_entry.disconnect(sid)
            self.g_heading_entry.disconnect(keyup_id)
            self.g_heading_entry.disconnect(keydown_sid)
            self.m_model.m_name = entry.get_text()
            self.g_heading.set_markup(u"<b>%s</b>" % entry.get_text())
            self.g_heading_entry.hide()
            self.g_heading.show()
        sid = self.g_heading_entry.connect('activate', finish_edit)
        def keydown(entry, event):
            if event.keyval == gtk.keysyms.Tab:
                finish_edit(entry)
        keydown_sid = self.g_heading_entry.connect('key-press-event', keydown)
        def keyup(entry, event):
            if event.keyval == gtk.keysyms.Escape:
                self.g_heading_entry.disconnect(sid)
                self.g_heading_entry.disconnect(keyup_id)
                self.g_heading_entry.hide()
                self.g_heading.show()
                return True
        keyup_id = self.g_heading_entry.connect('key-release-event', keyup)
    def on_add(self, btn, event):
        menu = gtk.Menu()
        item = gtk.MenuItem(_("Add link to new page"))
        item.connect('activate', self.on_add_link_to_new_page)
        menu.append(item)
        item = gtk.MenuItem(_("Add link to exercise"))
        item.connect('activate', self.on_add_link)
        menu.append(item)
        item = gtk.MenuItem(_("Add link by searching for exercises"))
        item.connect('activate', self.on_add_link_by_search)
        menu.append(item)
        menu.show_all()
        menu.popup(None, None, None, event.button, event.time)
    def on_remove(self, btn, event):
        self.m_parent.remove_section(self)
    def on_add_link_by_search(self, btn):
        dlg = SelectLessonfileBySearchDialog()
        while True:
            ret = dlg.run()
            if ret == gtk.RESPONSE_OK:
                self._add_filenames([os.path.abspath(lessonfile.uri_expand(dlg.m_filename))])
            else:
                break
        dlg.destroy()
    def on_add_link(self, btn):
        if editor_of(self).m_filename:
            open_dir = os.path.split(editor_of(self).m_filename)[0]
        else:
            open_dir = filesystem.user_data()
        dlg = SelectLessonFileDialog(editor_of(self))
        dlg.set_current_folder(open_dir)
        while 1:
            ret = dlg.run()
            if ret in (gtk.RESPONSE_REJECT, gtk.RESPONSE_DELETE_EVENT, gtk.RESPONSE_CANCEL):
                break
            else:
                assert ret == gtk.RESPONSE_OK
                self._add_filenames(dlg.get_filenames())
                break
        dlg.destroy()
    def _add_filenames(self, filenames):
        for filename in filenames:
            fn = gu.decode_filename(filename)
            assert os.path.isabs(fn)
            # If the file name is a file in a subdirectory below
            # lessonfile.exercises_dir in the current working directory,
            # then the file is a standard lesson file, and it will be
            # converted to a uri scheme with:
            fn = lessonfile.mk_uri(fn)
            # Small test to check that the file actually is a lesson file.
            try:
                lessonfile.infocache.get(fn, 'title')
            except lessonfile.infocache.FileNotLessonfile:
                continue
            self.m_model.append(fn)
            self.g_link_box.pack_start(self.create_linkrow(fn), False)
    def on_add_link_to_new_page(self, menuitem):
        page = pd.Page(_("Untitled%s") % "", [pd.Column()])
        self.m_model.append(page)
        self.g_link_box.pack_start(self.create_linkrow(page))
    def create_linkrow(self, link_this):
        hbox = gtk.HBox()
        def ff(btn, page):
            if id(page) in editor_of(self).m_page_mapping:
                editor_of(self).show_page_id(id(page))
            else:
                if not page[0]:
                    page[0].append(pd.LinkList(link_this.m_name))
                p = Page(page, parent_page(self))
                p.show()
                editor_of(self).add_page(p)
        if isinstance(link_this, pd.Page):
            linkbutton = gu.ClickableLabel(link_this.m_name)
            linkbutton.connect('clicked', ff, link_this)
        else:
            try:
                linkbutton = gu.ClickableLabel(lessonfile.infocache.get(link_this, 'title'))
                linkbutton.set_tooltip_text(link_this)
            except lessonfile.InfoCache.FileNotFound:
                linkbutton = gu.ClickableLabel(_(u"«%s» was not found") % link_this)
                linkbutton.make_warning()

        hbox.pack_start(linkbutton)
        linkbutton.connect('button-press-event', self.on_right_click_row, link_this)
        hbox.show_all()
        return hbox
    def on_right_click_row(self, button, event, linked):
        idx = self.m_model.index(linked)
        if event.button == 3:
            m = gtk.Menu()
            item = gtk.ImageMenuItem(gtk.STOCK_DELETE)
            item.connect('activate', self.on_delete_link, linked)
            m.append(item)
            item = gtk.ImageMenuItem(gtk.STOCK_CUT)
            item.connect('activate', self.on_cut_link, idx)
            m.append(item)
            item = gtk.ImageMenuItem(gtk.STOCK_PASTE)
            item.set_sensitive(bool(Editor.clipboard))
            item.connect('activate', self.on_paste, idx)
            m.append(item)
            item = gtk.ImageMenuItem(gtk.STOCK_EDIT)
            item.connect('activate', self.on_edit_linktext, linked)
            item.set_sensitive(bool(not isinstance(linked, basestring)))
            m.append(item)
            item = gtk.ImageMenuItem(gtk.STOCK_GO_UP)
            item.connect('activate', self.on_move_link_up, idx)
            item.set_sensitive(bool(idx > 0))
            m.append(item)
            item = gtk.ImageMenuItem(gtk.STOCK_GO_DOWN)
            item.connect('activate', self.on_move_link_down, idx)
            item.set_sensitive(bool(idx < len(self.m_model) - 1))
            m.append(item)
            item = gtk.ImageMenuItem(gtk.STOCK_EDIT)
            item.set_sensitive(isinstance(linked, unicode))
            item.connect('activate', self.on_edit_file, idx)
            m.append(item)
            m.show_all()
            m.popup(None, None, None, event.button, event.time)
            return True
    def on_delete_link(self, menuitem, linked):
        idx = self.m_model.index(linked)
        if id(linked) in editor_of(self).m_page_mapping:
            editor_of(self).destroy_window(id(linked))
        self.g_link_box.get_children()[idx].destroy()
        del self.m_model[idx]
    def on_edit_linktext(self, menuitem, linked):
        idx = self.m_model.index(linked)
        # row is the hbox containing the linkbutton
        row = self.g_link_box.get_children()[idx]
        linkbutton = row.get_children()[0]
        entry = gtk.Entry()
        entry.set_text(linkbutton.get_label())
        row.pack_start(entry)
        linkbutton.hide()
        entry.show()
        entry.grab_focus()
        def finish_edit(entry):
            linkbutton.set_label(entry.get_text().decode("utf-8"))
            linkbutton.get_children()[0].set_alignment(0.0, 0.5)
            linkbutton.show()
            self.m_model[idx].m_name = entry.get_text().decode("utf-8")
            entry.destroy()
        sid = entry.connect('activate', finish_edit)
        def keydown(entry, event):
            if event.keyval == gtk.keysyms.Tab:
                finish_edit(entry)
        entry.connect('key-press-event', keydown)
        def keyup(entry, event):
            if event.keyval == gtk.keysyms.Escape:
                linkbutton.show()
                entry.disconnect(sid)
                entry.destroy()
                return True
        entry.connect('key-release-event', keyup)
    def on_edit_file(self, menuitem, linked):
        try:
            try:
                subprocess.call((cfg.get_string("programs/text-editor"),
                             lessonfile.uri_expand(self.m_model[linked])))
            except OSError, e:
                 raise osutils.BinaryForProgramException("Text editor", cfg.get_string("programs/text-editor"), e)
        except osutils.BinaryForProgramException, e:
            solfege.win.display_error_message2(e.msg1, e.msg2)
    def on_cut(self, btn):
        self.m_parent.cut_section(self)
    def on_cut_link(self, menuitem, idx):
        Editor.clipboard.append(self.m_model[idx])
        del self.m_model[idx]
        self.g_link_box.get_children()[idx].destroy()
    def on_paste(self, btn, idx):
        assert Editor.clipboard, "Paste buttons should be insensitive when the clipboard is empty."
        pobj = Editor.clipboard.pop()
        if isinstance(pobj, pd.LinkList):
            mobj = pd.Page(pobj.m_name, [pd.Column(pobj)])
        else:
            mobj = pobj
        if idx == -1:
            self.m_model.append(mobj)
            self.g_link_box.pack_start(self.create_linkrow(mobj))
        else:
            self.m_model.insert(idx, mobj)
            row = self.create_linkrow(mobj)
            self.g_link_box.pack_start(row)
            self.g_link_box.reorder_child(row, idx)
    def on_move_link_up(self, btn, idx):
        """
        Move the link one row up.
        """
        assert idx > 0
        self.m_model[idx], self.m_model[idx - 1] = self.m_model[idx - 1], self.m_model[idx]
        self.g_link_box.reorder_child(self.g_link_box.get_children()[idx], idx - 1)
    def on_move_link_down(self, btn, idx=None):
        """
        Move the link one row down.
        """
        self.m_model[idx], self.m_model[idx + 1] = self.m_model[idx + 1], self.m_model[idx]
        self.g_link_box.reorder_child(self.g_link_box.get_children()[idx], idx + 1)


class Column(gtk.VBox):
    def __init__(self, model, parent):
        gtk.VBox.__init__(self)
        self.set_spacing(gu.hig.SPACE_MEDIUM)
        self.m_model = model
        self.m_parent = parent
        assert isinstance(model, pd.Column)
        self.g_section_box = gtk.VBox()
        self.g_section_box.set_spacing(gu.hig.SPACE_MEDIUM)
        self.pack_start(self.g_section_box, False)
        for section in model:
            assert isinstance(section, pd.LinkList)
            gui_section = Section(section, self)
            self.g_section_box.pack_start(gui_section, False)
        hbox = gtk.HBox()
        self.pack_start(hbox, False)
        b = gtk.Button(_("Add section"))
        hbox.pack_start(b, False)
        b.connect('clicked', self.on_add_section)
        b = gtk.Button(stock=gtk.STOCK_PASTE)
        b.connect('clicked', self.on_paste)
        Editor.clipboard.register_paste_button(b, pd.LinkList)
        hbox.pack_start(b, False)
    def __del__(self):
        logging.debug("Column.__del__")
    def cut_section(self, section):
        idx = self.g_section_box.get_children().index(section)
        Editor.clipboard.append(self.m_model[idx])
        del self.m_model[idx]
        self.g_section_box.get_children()[idx].destroy()
    def remove_section(self, section):
        idx = self.g_section_box.get_children().index(section)
        del self.m_model[idx]
        self.g_section_box.get_children()[idx].destroy()
    def on_add_section(self, btn):
        # We write "Untitled%s" % "" instead of just "Untitled" here
        # since "Untitled%s" is already translated in many languages.
        section = pd.LinkList(_("Untitled%s" % ""))
        self.m_model.append(section)
        gui_section = Section(section, self)
        self.g_section_box.pack_start(gui_section, False)
        gui_section.show_all()
    def move_section_down(self, widget, section):
        idx = self.g_section_box.get_children().index(section)
        if idx < len(self.g_section_box.get_children()) - 1:
            self.g_section_box.reorder_child(section, idx + 1)
            self.m_model[idx], self.m_model[idx + 1] \
                    = self.m_model[idx + 1], self.m_model[idx]
            self.m_parent.update_buttons()
    def move_section_up(self, widget, section):
        idx = self.g_section_box.get_children().index(section)
        if idx > 0:
            self.g_section_box.reorder_child(section, idx - 1)
            self.m_model[idx], self.m_model[idx - 1] \
                    = self.m_model[idx - 1], self.m_model[idx]
            self.m_parent.update_buttons()
    def on_paste(self, widget):
        """
        Paste the clipboard as a new section to this column.
        """
        assert Editor.clipboard, "Paste buttons should be insensitive when the clipboard is empty."
        assert isinstance(Editor.clipboard[-1], pd.LinkList)
        pobj = Editor.clipboard.pop()
        self.m_model.append(pobj)
        sect = Section(pobj, self)
        sect.show_all()
        self.g_section_box.pack_start(sect, False)


class Page(gtk.VBox):
    def __init__(self, model, parent):
        gtk.VBox.__init__(self)
        self.m_model = model
        self.m_parent = parent
        sc = gtk.ScrolledWindow()
        sc.set_policy(gtk.POLICY_AUTOMATIC, gtk.POLICY_AUTOMATIC)
        self.pack_start(sc)
        self.g_column_box = gtk.HBox()
        self.g_column_box.set_spacing(gu.hig.SPACE_LARGE)
        self.g_column_box.set_border_width(gu.hig.SPACE_SMALL)
        # We pack column into this box
        sc.add_with_viewport(self.g_column_box)
        self.show_all()
        if model:
            self.update_from_model()
    def __del__(self):
        logging.debug("Page.__del__:", self.m_model.m_name)
    def on_add_column(self, *btn):
        column = pd.Column()
        self.m_model.append(column)
        gcol = Column(column, self)
        gcol.show_all()
        self.g_column_box.pack_start(gcol)
    def on_move_section_left(self, button, section):
        column_idx = self.g_column_box.get_children().index(section.m_parent)
        section_idx = section.m_parent.g_section_box.get_children().index(section)
        if column_idx > 0:
            to_column = self.g_column_box.get_children()[column_idx - 1]
            section.reparent(to_column.g_section_box)
            section.m_parent = to_column
            to_column.g_section_box.set_child_packing(section, False, False, 0, gtk.PACK_START)
            self.m_model[column_idx - 1].append(self.m_model[column_idx][section_idx])
            del self.m_model[column_idx][section_idx]
            # Remove the right-most column if we moved the
            # last section out of it.
            if not self.g_column_box.get_children()[-1].g_section_box.get_children():
                assert len(self.m_model[-1]) == 0
                del self.m_model[-1]
                self.g_column_box.get_children()[-1].destroy()
            self.update_buttons()
    def on_move_section_right(self, button, section):
        # the column we move from
        column_idx = self.g_column_box.get_children().index(section.m_parent)
        section_idx = section.m_parent.g_section_box.get_children().index(section)
        if column_idx == len(self.g_column_box.get_children()) - 1:
            self.on_add_column()
        to_column = self.g_column_box.get_children()[column_idx + 1]
        section.reparent(to_column.g_section_box)
        section.m_parent = to_column
        to_column.g_section_box.set_child_packing(section, False, False, 0, gtk.PACK_START)
        to_section_idx = len(self.m_model[column_idx + 1])
        self.m_model[column_idx + 1].append(self.m_model[column_idx][section_idx])
        del self.m_model[column_idx][section_idx]
        self.update_buttons()
    def update_from_model(self):
        for child in self.g_column_box.get_children():
            child.destroy()
        for column in self.m_model:
            self.g_column_box.pack_start(Column(column, self))
        self.g_column_box.show_all()
        self.update_buttons()
    def update_buttons(self):
        num_cols = len(self.g_column_box.get_children())
        for col_idx, column in enumerate(self.g_column_box.get_children()):
            num_sects = len(column.g_section_box.get_children())
            for sect_idx, section in enumerate(column.g_section_box.get_children()):
                section.g_move_up_btn.set_sensitive(sect_idx != 0)
                section.g_move_down_btn.set_sensitive(sect_idx != num_sects -1)
                section.g_move_left_btn.set_sensitive(col_idx != 0)
                if [col for col in self.g_column_box.get_children() if not col.g_section_box.get_children()] and col_idx == num_cols - 1:
                    section.g_move_right_btn.set_sensitive(False)
                else:
                    section.g_move_right_btn.set_sensitive(True)


class Clipboard(list):
    def __init__(self, v=[]):
        list.__init__(v)
        self.m_paste_buttons = []
    def pop(self, i=-1):
        ret = list.pop(self, i)
        self.update_buttons()
        return ret
    def append(self, obj):
        list.append(self, obj)
        self.update_buttons()
    def register_paste_button(self, button, accepts_types):
        button.set_sensitive(bool(self) and isinstance(self[-1], accepts_types))
        self.m_paste_buttons.append((button, accepts_types))
    def update_buttons(self):
        for button, types in self.m_paste_buttons:
            button.set_sensitive(bool(self) and isinstance(self[-1], types))


class Editor(gtk.Window, gu.EditorDialogBase):
    savedir = os.path.join(filesystem.user_data(), u'exercises', u'user')
    # The clipboard will be shared between all Editor instances
    clipboard = Clipboard()
    def __init__(self, filename=None):
        gtk.Window.__init__(self)
        logging.debug("fpeditor.Editor.__init__(%s)" % filename)
        gu.EditorDialogBase.__init__(self, filename)
        self.set_default_size(800, 600)
        self.g_main_box = gtk.VBox()
        self.add(self.g_main_box)
        self.g_actiongroup.add_actions([
            ('GoBack', gtk.STOCK_GO_BACK, None, None, None, self.go_back),
        ])
        self.setup_toolbar()
        self.g_title_hbox = gtk.HBox()
        self.g_title_hbox.set_spacing(gu.hig.SPACE_SMALL)
        self.g_title_hbox.set_border_width(gu.hig.SPACE_SMALL)
        label = gtk.Label()
        label.set_markup(u"<b>%s</b>" % _("Front page title:"))
        self.g_title_hbox.pack_start(label, False)
        self.g_fptitle = gtk.Entry()
        self.g_title_hbox.pack_start(self.g_fptitle)
        self.g_main_box.pack_start(self.g_title_hbox, False)
        # This dict maps the windows created for all pages belonging to
        # the file.
        self.m_page_mapping = {}
        self.m_model = None
        if filename:
            self.load_file(filename)
        else:
            self.m_model = pd.Page(_("Untitled%s") % self.m_instance_number,
                    pd.Column())
            self.set_not_modified()
        self.add_page(Page(self.m_model, self))
        self.clipboard.update_buttons()
        self.show_all()
        self.add_to_instance_dict()
        self.g_fptitle.set_text(self.m_model.m_name)
        self.g_fptitle.connect('changed', self.on_frontpage_title_changed)
    def __del__(self):
        logging.debug("fpeditor.Editor.__del__, filename=" % self.m_filename)
    def add_page(self, page):
        """
        Add and show the page.
        """
        editor_of(self).m_page_mapping[id(page.m_model)] = page
        self.g_main_box.pack_start(page)
        self.show_page(page)
    def show_page_id(self, page_id):
        self.show_page(self.m_page_mapping[page_id])
    def show_page(self, page):
        """
        Hide the currently visible page, and show PAGE instead.
        """
        try:
            self.g_visible_page.hide()
        except AttributeError:
            pass
        self.g_visible_page = page
        page.show()
        if isinstance(page.m_parent, Page):
            self.g_title_hbox.hide()
        else:
            self.g_title_hbox.show()
        self.g_ui_manager.get_widget("/Toolbar/GoBack").set_sensitive(
            not isinstance(self.g_visible_page.m_parent, Editor))
    def go_back(self, *action):
        self.show_page(self.g_visible_page.m_parent)
    def on_frontpage_title_changed(self, widget):
        self.m_model.m_name = widget.get_text()
    def setup_toolbar(self):
        self.g_ui_manager.insert_action_group(self.g_actiongroup, 0)
        uixml = """
        <ui>
         <toolbar name='Toolbar'>
          <toolitem action='GoBack'/>
          <toolitem action='New'/>
          <toolitem action='Open'/>
          <toolitem action='Save'/>
          <toolitem action='SaveAs'/>
          <toolitem action='Close'/>
          <toolitem action='Help'/>
         </toolbar>
         <accelerator action='Close'/>
         <accelerator action='New'/>
         <accelerator action='Open'/>
         <accelerator action='Save'/>
        </ui>
        """
        self.g_ui_manager.add_ui_from_string(uixml)
        toolbar = self.g_ui_manager.get_widget("/Toolbar")
        self.g_main_box.pack_start(toolbar, False)
        self.g_main_box.reorder_child(toolbar, 0)
        self.g_ui_manager.get_widget("/Toolbar").set_style(gtk.TOOLBAR_BOTH)
    def destroy_window(self, window_id):
        """
        Destroy the window with the id 'windowid' and all subwindows.
        """
        def do_del(wid):
            for key in [k for k in self.m_page_mapping.keys()
                    if id(parent_page(self.m_page_mapping[k]).m_model) == wid]:
                do_del(key)
            editor_of(self).m_page_mapping[wid].destroy()
            del editor_of(self).m_page_mapping[wid]
        do_del(window_id)
    @staticmethod
    def edit_file(fn):
        if fn in Editor.instance_dict:
            Editor.instance_dict[fn].present()
        else:
            try:
                win = Editor(fn)
                win.show()
            except IOError, e:
                gu.dialog_ok(_("Loading file '%(filename)s' failed: %(msg)s") %
                        {'filename': fn, 'msg': str(e).decode('utf8', 'replace')})
    def load_file(self, filename):
        """
        Load a file into a empty, newly created Editor object.
        """
        assert self.m_model == None
        self.m_model = pd.load_tree(filename, C_locale=True)
        self.m_filename = filename
        #
        if not os.path.isabs(filename):
            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()
        self.set_not_modified()
        self.set_title(self.m_filename)
    def set_not_modified(self):
        """
        Store the current state of the data in self.m_orig_dump so that
        is_modified() will return False until we make new changes.
        """
        io = StringIO.StringIO()
        self.m_model.dump(io)
        self.m_orig_dump = io.getvalue()
    def is_modified(self):
        """
        Return True if the data has changed since the last call to
        set_not_modified()
        """
        io = StringIO.StringIO()
        self.m_model.dump(io)
        s = io.getvalue()
        return s != self.m_orig_dump
    @property
    def m_changed(self):
        return self.is_modified()
    def save(self, w=None):
        assert self.m_filename
        save_location = os.path.split(self.m_filename)[0] + os.sep
        fh = pd.FileHeader(1, self.m_model)
        fh.save_file(self.m_filename)
        self.set_not_modified()
        # We do test for solfege.win since it is not available during testing
        if getattr(solfege, 'win', None):
            solfege.win.load_frontpage()
    def on_show_help(self, *w):
        return
    def get_save_as_dialog(self):
        dialog = gu.EditorDialogBase.get_save_as_dialog(self)
        ev2 = gtk.EventBox()
        ev2.set_name("DIALOGWARNING2")
        ev = gtk.EventBox()
        ev.set_border_width(gu.hig.SPACE_SMALL)
        ev2.add(ev)
        ev.set_name("DIALOGWARNING")
        label = gtk.Label()
        label.set_padding(gu.hig.SPACE_MEDIUM, gu.hig.SPACE_MEDIUM)
        ev.add(label)
        label.set_markup(_("<b>IMPORTANT:</b> Your front page file <b>must</b> be saved in a subdirectory below the directory named exercises. See the user manual for details."))
        dialog.set_extra_widget(ev2)
        ev2.show_all()
        return dialog

if __name__ == '__main__':
    gtk.link_button_set_uri_hook(lambda a, b: None)
    e = Editor()
    e.load_file("learningtrees/learningtree.txt")
    gtk.main()
