# vim: set fileencoding=utf-8 :
# GNU Solfege - free ear training software
# Copyright (C) 2000, 2001, 2002, 2003, 2004, 2005, 2006, 2007, 2008  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.handlers

from solfege import optionparser

import solfege

opt_parser = optionparser.SolfegeOptionParser()
options, args = opt_parser.parse_args()

if options.debug:
    # We import all exercise modules here even though it is not necessary
    # since the modules are loaded one by one when needed. But by importing
    # all here, we catch SyntaxErrors at once.
    from solfege.exercises import *

    handler = logging.StreamHandler()
    logging.getLogger().addHandler(handler)
    logging.getLogger().setLevel(logging.DEBUG)
else:
    handler = logging.handlers.MemoryHandler(1)
    logging.getLogger().addHandler(handler)


import webbrowser
import textwrap
# We move x-www-browser to the end of the list because on my
# debian etch system, the browser does will freeze solfege until
# I close the browser window.
try:
    i = webbrowser._tryorder.index("x-www-browser")
    webbrowser._tryorder.append(webbrowser._tryorder[i])
    del webbrowser._tryorder[i]
except ValueError:
    pass

import sys
import traceback
import os
import time
import urllib
import shutil

from solfege import winlang
from solfege import runtime
from solfege import buildinfo
from solfege.exceptiondialog import ExceptionDialog

solfege_copyright = u"Copyright © 1999-2008 Tom Cato Amundsen <tca@gnu.org>, and others."

warranty = """
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 solfege import osutils
from solfege import application

if options.version:
    if buildinfo.is_release():
        rev_info = ""
    else:
        rev_info = u"\n    " + u"\n    ".join(buildinfo.get_bzr_revision_info_list())
    print (u"""GNU Solfege %s%s
This is free software. It is covered by the GNU General Public License,
and you are welcome to change it and/or distribute copies of it under
certain conditions. Invoke as `solfege --warranty` for more information.

%s
        """ % (buildinfo.VERSION_STRING,
        rev_info,
        solfege_copyright)).encode(sys.getfilesystemencoding(), 'replace')
    sys.exit()

if options.warranty:
    print "GNU Solfege %s" % buildinfo.VERSION_STRING
    print solfege_copyright.encode(sys.getfilesystemencoding(), 'replace')
    print warranty
    sys.exit()

# silent, GNOME, be silent!
#sys.argv.append('--disable-sound')
runtime.init(options)

import gtk
# We need to set a uri hook that does nothing, since ubuntu 9.04 rc give
# GConf errors and GtkWarnings without it.
gtk.link_button_set_uri_hook(lambda a, b: None)

from solfege import utils
from solfege import i18n

class SplashWin(gtk.Window):
    def __init__(self):
        gtk.Window.__init__(self, gtk.WINDOW_POPUP)
        self.set_position(gtk.WIN_POS_CENTER)
        self.set_resizable(True)
        frame = gtk.Frame()
        frame.set_shadow_type(gtk.SHADOW_OUT)
        self.add(frame)
        vbox = gtk.VBox()
        vbox.set_border_width(20)
        frame.add(vbox)
        l = gtk.Label(_("Starting GNU Solfege %s") % buildinfo.VERSION_STRING)
        l.set_name("Heading1")
        vbox.pack_start(l)
        l = gtk.Label("http://www.solfege.org")
        vbox.pack_start(l)
        self.g_infolabel = gtk.Label('')
        vbox.pack_start(self.g_infolabel)
        self.show_all()
    def show_progress(self, txt):
        self.g_infolabel.set_text(txt)
        while gtk.events_pending():
            gtk.main_iteration(0)

if not options.no_splash:
    splash_win = SplashWin()
    time.sleep(0.1)
    gtk.gdk.flush()
    while gtk.events_pending():
        gtk.main_iteration(0)
else:
    splash_win = None

if splash_win:
    splash_win.show_progress(_("Importing application modules"))

from solfege import tracebackwindow
# redirect error messages to a window that will popup if
# something bad happens.

sys.stderr = tracebackwindow.TracebackWindow(options.show_gtk_warnings)

from solfege.configwindow import ConfigWindow
from solfege.mpd.musicdisplayer import MusicDisplayer
from solfege import gu
from solfege import cfg
from solfege import stock
from solfege import lessonfile
from solfege import lessonfilegui
from solfege import abstract


from solfege import frontpage
from solfege import fpeditor
from solfege.trainingsetdlg import TrainingSetDialog
from solfege.practisesheetdlg import PractiseSheetDialog
from solfege import filesystem
from solfege import statistics

class MusicViewerWindow(gtk.Dialog):
    def __init__(self):
        gtk.Dialog.__init__(self)
        self.set_default_size(500, 300)
        self.g_music_displayer = MusicDisplayer()
        self.vbox.pack_start(self.g_music_displayer)
        b = gu.bButton(self.action_area, _("Close"), solfege.win.close_musicviewer)
        b.grab_focus()
        self.connect('destroy', solfege.win.close_musicviewer)
        self.show_all()
    def display_music(self, music):
        fontsize = cfg.get_int('config/feta_font_size=20')
        self.g_music_displayer.display(music, fontsize)

class SelectWinBase(gtk.ScrolledWindow):
    """
    Base class for the classes that display the front page and the
    page where we select exerises or tests.
    """
    def __init__(self):
        gtk.ScrolledWindow.__init__(self)
        self.set_policy(gtk.POLICY_AUTOMATIC, gtk.POLICY_AUTOMATIC)
        self._on_focus_in_blocked = False
        self.m_min_width = 400
        self.m_min_height = 300
        self.set_size_request(self.m_min_width, self.m_min_height)
        self.m_linkbuttons = []
        self.g_searchentry = None
    def on_focus_in(self, w, event):
        """
        Set the vadjustment so that the window will scroll to make the button
        that has the focus visible in the scrolled window.
        """
        if self._on_focus_in_blocked:
            return
        a = w.get_allocation()
        adj = self.get_vadjustment()
        if a.y + a.height > adj.value + adj.page_size:
            if w.get_data('last'):
                adj.value = adj.upper - adj.page_size
            else:
                adj.value = a.y - adj.page_size + a.height
        elif a.y < adj.value:
            # If the widget is the first row of the first category, then...
            if w.get_data('first'):
                adj.value = 0.0
            else:
                adj.value = a.y
    def on_key_press_event(self, mainwin, event):
        if event.type == gtk.gdk.KEY_PRESS and event.state == 0:
            adj = self.get_vadjustment()
            if event.keyval in (gtk.keysyms.End, gtk.keysyms.KP_End):
                if self.m_linkbuttons:
                    self.m_linkbuttons[-1].grab_focus()
                return True
            elif event.keyval in (gtk.keysyms.Home, gtk.keysyms.KP_Home):
                if self.g_searchentry:
                    self.get_vadjustment().value = 0.0
                    self.g_searchentry.grab_focus()
                else:
                    self.m_linkbuttons[0].grab_focus()
                return True
            if event.keyval in (gtk.keysyms.Page_Down, gtk.keysyms.KP_Page_Down):
                # First we find the button that now has the keyboard focus
                for idx, btn in enumerate(self.m_linkbuttons):
                    if btn.is_focus():
                        break
                # Then we find a button that is approximately adj.page_increment
                # down and grabs that buttons focus.
                for to_btn in self.m_linkbuttons[idx:]:
                    if to_btn.get_allocation().y - adj.value > adj.page_size:
                        self._on_focus_in_blocked = True
                        to_btn.grab_focus()
                        self._on_focus_in_blocked = False
                        return True
                self.m_linkbuttons[-1].grab_focus()
                return True
            if event.keyval in (gtk.keysyms.Page_Up, gtk.keysyms.KP_Page_Up):
                for idx, btn in enumerate(self.m_linkbuttons):
                    if btn.is_focus():
                        break
                for to_btn in self.m_linkbuttons:
                    if to_btn.get_allocation().y > adj.value - adj.page_increment:
                        self._on_focus_in_blocked = True
                        to_btn.grab_focus()
                        self._on_focus_in_blocked = False
                        return True
    def adjust_scrolledwin_size(self):
        # We do set_size_request on the view to make the window so wide
        # that we avoid a horizontal scroll bar.
        w = self.g_box.size_request()[0]
        # It should be possible to check if the vscrollbar is visible, and
        # only reserve space for it. But I cannot get that to works. I find
        # that self.get_vscrollbar().props.visible is updated only part
        # of the time.
        w += self.get_vscrollbar().size_request()[0]
        # FIXME I don't understand why we have to add the extra
        # two pixels to avoid the horizontal scrollbar in all cases.
        w += self.style_get_property("scrollbar-spacing") + 2
        if w < self.m_min_width:
            w = self.m_min_width
        self.set_size_request(w, self.m_min_height)


class ExerciseView(SelectWinBase):
    max_exercise_label_width = int(gtk.gdk.Screen().get_width() * 0.90)
    def __init__(self):
        SelectWinBase.__init__(self)
        app = solfege.app
        self.g_box = gtk.VBox()
        self.g_box.set_border_width(gu.hig.SPACE_MEDIUM)
        self.add_with_viewport(self.g_box)
        self.g_searchbox = gtk.HBox()
        self.g_box.pack_start(self.g_searchbox, False, padding=gu.hig.SPACE_MEDIUM)
        self.g_searchentry = gtk.Entry()
        self.g_searchbox.pack_start(self.g_searchentry, True)
        self.g_searchentry.connect('activate', self.on_search)
        gu.bButton(self.g_searchbox, _("Search"), callback=self.on_search, expand=False)
        self.show_all()
    def _display_data(self, page, display_only_tests=False,
            is_search_result=False, show_topics=False):
        """
        display_only_tests should be True when we are browsing available tests.
        This will validate statistics for each lesson and display the test result.
        """
        if not is_search_result:
            self.m_page = page
        try:
            self.g_hbox.destroy()
        except AttributeError:
            pass
        self.g_hbox = gu.bHBox(self.g_box)
        self.g_hbox.set_spacing(gu.hig.SPACE_MEDIUM)
        label = None
        for column in page:
            sizegroup = gtk.SizeGroup(gtk.SIZE_GROUP_HORIZONTAL)
            assert isinstance(column, frontpage.Column)
            first = True
            vbox = gtk.VBox()
            vbox.set_spacing(gu.hig.SPACE_MEDIUM)
            self.g_hbox.pack_start(vbox, False)
            for sect_id, linklist in enumerate(column):
                if isinstance(linklist, frontpage.Paragraph):
                    label = gtk.Label(linklist.m_text)
                    label.set_line_wrap(True)
                    label.set_alignment(0.0, 0.5)
                    vbox.pack_start(label, False)
                    continue
                assert isinstance(linklist, frontpage.LinkList)
                if (display_only_tests
                    and not frontpage._TreeCommon.tests_in_sub(linklist)):
                        continue
                linkbox = gtk.VBox()
                vbox.pack_start(linkbox, False)
                heading = gtk.Label("<b>%s</b>" % _no_xgettext(linklist.m_name))
                heading.set_alignment(0.0, 0.5)
                heading.set_use_markup(True)
                linkbox.pack_start(heading, False)
                for idx, link in enumerate(linklist):
                    if isinstance(self, TestsView) and not frontpage._TreeCommon.tests_in_sub(link):
                        continue
                    if isinstance(link, frontpage.Page):
                        label = gu.ClickableLabel(_no_xgettext(link.m_name))
                        label.connect('clicked', self.on_page_link_clicked, link)
                    else:
                        assert isinstance(link, unicode), type(link)
                        if display_only_tests:
                            solfege.db.validate_stored_statistics(link)
                        try:
                            labeltxt = lessonfile.infocache.get(link, 'title')
                            label = gu.ClickableLabel(_no_xgettext(labeltxt))
                            if show_topics:
                                topic = solfege.app.m_frontpage_data.get_topic_of_lesson_file(link)
                                if topic and topic not in labeltxt:
                                    label.add_heading(topic)
                            label.connect('clicked', self.on_link_clicked, link)
                        except lessonfile.InfoCache.FileNotFound:
                            label = gu.ClickableLabel(_(u"«%s» was not found") % link)
                            label.make_warning()
                        except lessonfile.InfoCache.FileNotLessonfile:
                            label = gu.ClickableLabel(_(u"Failed to parse «%s»") % link)
                            label.make_warning()
                    if first:
                        label.set_data('first', True)
                        first = False
                    w = label.size_request()[0]
                    if w > self.max_exercise_label_width:
                        w = self.max_exercise_label_width
                        if isinstance(link, unicode):
                            txt = _(lessonfile.infocache.get(link, "title"))
                        else:
                            txt = link.m_name
                        label.set_tooltip_text(txt)
                        label.set_size_request(w / len(page), -1)
                    self.m_linkbuttons.append(label)
                    label.connect('focus-in-event', self.on_focus_in)
                    if display_only_tests:
                        box = gtk.HBox()
                        box.pack_start(label)
                        if isinstance(link, unicode):
                            passed, result = solfege.db.get_test_status(link)
                            if passed == True:
                                box.pack_start(gtk.Label(_("passed, %.1f%%") % (result * 100)))
                            if passed == False:
                                box.pack_start(gtk.Label(_("failed, %.1f%%") % (result * 100)))
                            # do nothing if passed == None since 
                        linkbox.pack_start(box, True)
                    else:
                        linkbox.pack_start(label, True)
                    sizegroup.add_widget(label)
            if label:
                label.set_data('last', True)
        self.g_hbox.show_all()
        self.adjust_scrolledwin_size()
        self.get_vadjustment().value = 0.0
    def on_page_link_clicked(self, btn, link):
        self.g_searchbox.hide()
        self.display_data(link)
    def on_link_clicked(self, w, filename):
        solfege.app.practise_lessonfile(filename)
    def display_search_result(self, searchfor, result, result_C, display_only_tests=False):
        self._display_data(
          frontpage.Page(u'',
            frontpage.Column([
              frontpage.LinkList(
            _("Search results for “%s”:" % self.g_searchentry.get_text()),
              result),
              frontpage.LinkList(
            _("C-locale search results for “%s”:" % self.g_searchentry.get_text()),
              result_C),
              ])))
    def on_end_practise(self, w=None):
        pass
    def _search(self, substring, C_locale, only_tests):
        """
        substring - the string to search for
        C_locale - True if we should search in the untranslated titles
        only_tests - True if we should only return exercises that have
                     tests
        """
        logging.debug("search: '%s'" % substring)
        match_func = {
            False: lambda filename: substring in _no_xgettext(lessonfile.infocache.get(filename, 'title')).lower(),
            True: lambda filename: substring in lessonfile.infocache.get(filename, 'title').lower()
        }[C_locale]
        test_filter = {
            False: lambda filename: True,
            True: lambda filename: lessonfile.infocache.get(filename, 'test')
        }[C_locale]
        page = frontpage.Page(listitems=frontpage.Column())
        cur_topic = None
        # the last topic appended to the page
        last_topic = None 
        found = set()
        for child in self.m_page.iterate_flattened():
            if isinstance(child, frontpage.LinkList):
                cur_topic = child
            if isinstance(child, unicode):
                try:
                    if (match_func(child) and test_filter(child)) and child not in found:
                        if cur_topic != last_topic:
                            page[0].append(frontpage.LinkList(cur_topic.m_name))
                            last_topic = cur_topic
                        found.add(child)
                        page[0][-1].append(child)
                except lessonfile.infocache.InfoCacheException:
                    # Ignore missing lesson files and files with errors
                    pass
        return page


class FrontPage(ExerciseView):
    def display_data(self, data, display_only_tests=False,
            is_search_result=False, show_topics=False):
        self._display_data(data, display_only_tests, is_search_result,
                           show_topics)
        self.g_searchentry.grab_focus()
    def on_search(self, *button):
        search_for = self.g_searchentry.get_text().lower()
        logging.debug("FrontPage.on_search '%s'" % search_for)
        page = self._search(search_for, False, False)
        page_C = self._search(search_for, True, False)
        page_C.m_name = _('Search untranslated lesson titles')
        if not page_C.is_empty():
            page[0].append(frontpage.LinkList(_('Too few matches?'),
                listitems=[page_C]))
        self.display_data(page, is_search_result=True)


class TestsView(ExerciseView):
    def on_link_clicked(self, w, filename):
        solfege.app.test_lessonfile(filename)
    def display_data(self, data=None, is_search_result=False):
        self._display_data(data, True, is_search_result)
        self.g_searchentry.grab_focus()
    def on_search(self, *button):
        search_for = self.g_searchentry.get_text().lower()
        page = self._search(search_for, False, True)
        page_C = self._search(search_for, True, True)
        page_C.m_name = u'Search exercises without translating them'
        if not page_C.is_empty():
            page[0].append(frontpage.LinkList(_('Too few matches?'),
                listitems=[page_C]))
        self.display_data(page, is_search_result=True)


class SearchView(ExerciseView):
    def __init__(self):
        ExerciseView.__init__(self)
        page = """FileHeader(1, 
        Page(u'', [
         Column([
          Paragraph(u'Search the exercise titles of all lesson files found by the program, not just the active front page with sub pages.'),
         ]),
        ])
        )"""
        p = frontpage.parse_tree(page)
        self.display_data(frontpage.parse_tree(page))
        self.g_searchentry.show()
    def display_data(self, data):
        self._display_data(data)
        self.g_searchentry.grab_focus()
    def on_search(self, *button):
        search_for = self.g_searchentry.get_text().lower()
        lessonfile.infocache.update_modified_files()
        self.display_search_result(self.g_searchentry.get_text(), [
            filename for filename in lessonfile.infocache._data
            if search_for in _(lessonfile.infocache.get(filename, 'title')).lower()
        ], [
            filename for filename in lessonfile.infocache._data
            if search_for in lessonfile.infocache.get(filename, 'title').lower()
        ])
    

class MainWin(gtk.Window, cfg.ConfigUtils):
    default_front_page = os.path.join(lessonfile.exercises_dir, 'learningtree.txt')
    debug_front_page = os.path.join(lessonfile.exercises_dir, 'debugtree.txt')
    def __init__(self, options, datadir):
        gtk.Window.__init__(self, gtk.WINDOW_TOPLEVEL)
        self._vbox = gtk.VBox()
        self._vbox.show()
        self.add(self._vbox)
        stock.SolfegeIconFactory(self, datadir)
        gtk.settings_get_default().set_property('gtk-button-images', True)
        pixbuf = self.render_icon('solfege-icon', gtk.ICON_SIZE_DIALOG)
        gtk.window_set_default_icon(pixbuf)
        cfg.ConfigUtils.__dict__['__init__'](self, 'mainwin')
        self.set_resizable(self.get_bool('gui/mainwin_user_resizeable'))
        self.add_watch('gui/mainwin_user_resizeable', lambda s: self.set_resizable(self.get_bool('gui/mainwin_user_resizeable')))
        self.connect('delete-event', self.quit_program)
        self.connect('key_press_event', self.on_key_press_event)
        self.g_about_window = None
        self.m_exercise = None
        self.m_viewer = None
        self.box_dict = {}
        self.g_config_window = None
        self.g_path_info_dlg = None
        self.g_musicviewer_window = None
        self.g_ui_manager = gtk.UIManager()
        self.m_action_groups = {
            'Exit': gtk.ActionGroup('Exit'),
            'NotExit': gtk.ActionGroup('NotExit'),
        }
        for a in self.m_action_groups.values():
            self.g_ui_manager.insert_action_group(a, 1)
        self.setup_menu()
        self.main_box = gtk.VBox()
        self.main_box.show()
        self._vbox.pack_start(self.main_box)
        self.on_frontpage_changed()
        self.display_frontpage()
    def get_view(self):
        """
        Return the view that is currently visible.
        Raise KeyError if no view has yet been added.
        """
        return self.box_dict[self.m_viewer]
    def add_view(self, view, name):
        """
        Hide the current view.
        Add and view the new view.
        """
        assert name not in self.box_dict
        if self.m_viewer:
            self.get_view().hide()
        self.box_dict[name] = view
        self.main_box.pack_start(self.box_dict[name])
        self.box_dict[name].show()
        self.m_viewer = name
    def show_view(self, name):
        """
        Return False if the view does not exist.
        Hide the current visible view, show the view named 'name' and
        return True.
        """
        if name not in self.box_dict:
            return False
        self.get_view().hide()
        self.m_viewer = name
        self.box_dict[name].show()
        return True
    def change_frontpage(self, filename):
        """
        Change to a different frontpage file.
        """
        self.set_string('app/frontpage', filename)
        self.on_frontpage_changed()
    def on_frontpage_changed(self, *v):
        """
        Load the frontpage data from the file specified by the config
        variable "app/frontpage"
        """
        filename = self.get_string("app/frontpage")
        if filename == self.debug_front_page and not solfege.app.m_options.debug:
            self.set_string("app/frontpage", self.default_front_page)
            filename = self.default_front_page
        if not os.path.isfile(filename):
            filename = self.default_front_page
        try:
            solfege.app.m_frontpage_data = frontpage.load_tree(filename)
        except Exception, e:
            if splash_win:
                splash_win.hide()
            solfege.app.m_frontpage_data = frontpage.load_tree(self.default_front_page)
            self.set_string('app/frontpage', self.default_front_page)
            gu.dialog_ok(_("Loading front page '%s' failed. Using default page." % filename),
                secondary_text = "\n".join(traceback.format_exception(*sys.exc_info())))
            if splash_win:
                splash_win.show()
        self.display_frontpage()
    def setup_menu(self):
        self.m_action_groups['Exit'].add_actions([
          ('FileMenu', None, _('_File')),
          ('AppQuit', 'gtk-quit', None, None, None, self.quit_program),
        ])
        self.m_action_groups['NotExit'].add_actions([
          ('TheoryMenu', None, _('The_ory')),
          ('FrontPagesMenu', None, _('Sele_ct Front Page')),
          ('TheoryIntervals', None, _('_Intervals'), None, None,
            lambda o: solfege.app.handle_href('theory-intervals.html')),
          ('TreeEditor', None, _('_Edit Front Page'), None, None,
            self.do_tree_editor),
          ('ExportTrainingSet', None, _('E_xport Exercises to Audio Files…'), None, None,
            self.new_training_set_editor),
          ('EditPractiseSheet', None, _('Ear Training Test Pri_ntout…'), None, None,
            self.new_practisesheet_editor),
          ('OpenPreferencesWindow', 'gtk-preferences', None, '<ctrl>F12', None,
            self.open_preferences_window),
          ('HelpMenu', None, _('_Help')),
          ('Search', 'gtk-search', _('_Search Exercises'), '<ctrl>F', None,
              self.on_search_all_exercises),
          ('FrontPage', None, _('_Front Page'), 'F5', None,
              lambda w: self.display_frontpage()),
          ('TestsPage', None, _('_Tests Page'), 'F6', None,
              self.display_testpage),
          ('RecentExercises', None, _('_Recent Exercises'), 'F7', None,
              self.display_recent_exercises),
          ('UserExercises', None, _('_User Exercises'), 'F8', None,
              self.display_user_exercises),
          ('HelpHelp', 'gtk-help', _('_Help on the current exercise'), 'F1', None,
            lambda o: solfege.app.please_help_me()),
          ('HelpTheory', None, _('_Music theory on the current exercise'), 'F3', None, lambda o: solfege.app.show_exercise_theory()),
          ('HelpIndex', None, _('_User manual'), None, None,
            lambda o: solfege.app.handle_href('index.html')),
          ('HelpShowPathInfo', None, _('_File locations'), None,
            None, self.show_path_info),
          ('HelpOnline', None, _('_Mailing lists, web page etc.'), None, None,
            lambda o: solfege.app.handle_href('online-resources.html')),
          ('HelpReportingBugs', None, _('Reporting _bugs'), None, None,
            lambda o: solfege.app.handle_href('bug-reporting.html')),
          ('HelpAbout', 'gtk-about', None, None, None, self.show_about_window),
          ('ShowBugReports', None, _('_See your bug reports'), None, None,
            self.show_bug_reports),
        ])

        self.g_ui_manager.add_ui_from_file("ui.xml")

        self.add_accel_group(self.g_ui_manager.get_accel_group())
        hdlbox = gtk.HandleBox()
        hdlbox.show()
        hdlbox.add(self.g_ui_manager.get_widget('/Menubar'))
        self._vbox.pack_start(hdlbox, False)
        self.m_help_on_current_merge_id = None
    def create_frontpage_menu(self):
        """
        Create, or update if already existing, the submenu that let the
        user choose which front page file to display.
        """
        if self.m_frontpage_merge_id:
            self.g_ui_manager.remove_ui(self.m_frontpage_merge_id)
        actions = []
        old_dir = None
        s = "<menubar name='Menubar'><menu action='FileMenu'><menu action='FrontPagesMenu'>"
        for fn in frontpage.get_front_pages_list(solfege.app.m_options.debug):
            if not frontpage.may_be_frontpage(fn):
                continue
            cur_dir = os.path.split(fn)[0]
            if old_dir != cur_dir:
                s += '<separator name="sep@%s"/>' % fn
                old_dir = cur_dir
            s += "<menuitem action='%s'/>\n" % fn
            if not self.m_action_groups['NotExit'].get_action(fn):
                actions.append((fn, None, lessonfile.infocache.frontpage.get(fn, 'title'), None, fn,
                lambda o, f=fn: self.change_frontpage(f)))
            else:
                action = self.m_action_groups['NotExit'].get_action(fn)
                action.props.label = lessonfile.infocache.frontpage.get(fn, 'title')
        s += "</menu></menu></menubar>"
        self.m_action_groups['NotExit'].add_actions(actions)
        self.m_frontpage_merge_id = self.g_ui_manager.add_ui_from_string(s)
    def show_help_on_current(self):
        """
        Show the menu entries for the exercise help and music theory
        pages on the Help menu.
        """
        if self.m_help_on_current_merge_id:
            return
        self.m_help_on_current_merge_id = self.g_ui_manager.add_ui_from_string("""
<menubar name='Menubar'>
  <menu action='HelpMenu'>
    <placeholder name='PerExerciseHelp'>
      <menuitem position='top' action='HelpHelp' />
      <menuitem action='HelpTheory' />
    </placeholder>
  </menu>
</menubar>""")
    def hide_help_on_current(self):
        """
        Hide the menu entries for the help and music theory pages on the
        Help menu.
        """
        if not self.m_help_on_current_merge_id:
            return
        self.g_ui_manager.remove_ui(self.m_help_on_current_merge_id)
        self.m_help_on_current_merge_id = None
    def show_bug_reports(self, *v):
        m = gtk.Dialog(_("Question"), self, 0)
        m.add_button(gtk.STOCK_CANCEL, gtk.RESPONSE_CANCEL)
        m.add_button(gtk.STOCK_OK, gtk.RESPONSE_OK)
        vbox = gtk.VBox()
        m.vbox.pack_start(vbox, False)
        vbox.set_spacing(18)
        vbox.set_border_width(12)
        l = gtk.Label(_("Please enter the email used when you submitted the bugs:"))
        vbox.pack_start(l, False)
        self.g_email = gtk.Entry()
        m.action_area.get_children()[0].grab_default()
        self.g_email.set_activates_default(True)
        vbox.pack_start(self.g_email, False)
        m.show_all()
        ret = m.run()
        m.destroy()
        if ret == gtk.RESPONSE_OK:
            params = urllib.urlencode({
                    'pagename': 'SITS-Incoming/SearchBugs',
                    'q': 'SITS-Incoming/"Submitter: %s"' % utils.mangle_email(self.g_email.get_text()),
                })
            try:
                webbrowser.open_new("http://www.solfege.org?%s" % params)
            except Exception, e:
                self.display_error_message2(_("Error opening web browser"), str(e))
    def display_exception_message(self, exception, lessonfile=None):
        """Call this function only inside an except clause."""
        sourcefile, lineno, func, code = traceback.extract_tb(sys.exc_info()[2])[0]
        # We can replace characters because we will only display the
        # file name, not open the file.
        sourcefile = sourcefile.decode(sys.getfilesystemencoding(), 'replace')
        m = ExceptionDialog(exception)
        if lessonfile:
            m.add_text(_("Please check the lesson file %s.") % lessonfile)
        if sourcefile:
            m.add_text(_('The exception was caught in\n"%(filename)s", line %(lineno)i.') % {'filename': sourcefile, 'lineno': lineno})
        if 'm_nonwrapped_text' in dir(exception):
            m.add_nonwrapped_text(exception.m_nonwrapped_text)
        m.run()
        m.destroy()
    def display_error_message2(self, text, secondary_text):
        """
        This is the new version of display_error_message, and it will
        eventually replace the old.
        """
        if splash_win and splash_win.props.visible:
            splash_win.hide()
            reshow_splash = True
        else:
            reshow_splash = False
        if not isinstance(text, unicode):
            text = text.decode(locale.getpreferredencoding(), 'replace')
        if not isinstance(secondary_text, unicode):
            secondary_text = secondary_text.decode(locale.getpreferredencoding(), 'replace')
        m = gtk.MessageDialog(None, gtk.DIALOG_MODAL, gtk.MESSAGE_ERROR,
                              gtk.BUTTONS_CLOSE, text)
        if secondary_text:
            m.format_secondary_text(secondary_text)
        m.run()
        m.destroy()
        if reshow_splash:
            splash_win.show()
            while gtk.events_pending():
                gtk.main_iteration(0)
    def display_error_message(self, msg, title=None, secondary_text=None):
        if splash_win and splash_win.props.visible:
            splash_win.hide()
            reshow_splash = True
        else:
            reshow_splash = False
        if not isinstance(msg, unicode):
            msg = msg.decode(locale.getpreferredencoding(), 'replace')
        m = gtk.MessageDialog(None, gtk.DIALOG_MODAL, gtk.MESSAGE_ERROR,
                              gtk.BUTTONS_CLOSE, None)
        m.set_markup(gu.escape(msg))
        if title:
            m.set_title(title)
        if secondary_text:
            m.format_secondary_text(secondary_text)
        m.run()
        m.destroy()
        if reshow_splash:
            splash_win.show()
            while gtk.events_pending():
                gtk.main_iteration(0)
    def show_path_info(self, w):
        if not self.g_path_info_dlg:
            self.g_path_info_dlg = gtk.Dialog(_("File locations"), self,
                buttons=(gtk.STOCK_OK, gtk.RESPONSE_ACCEPT))
            vbox = gu.hig_dlg_vbox()
            self.g_path_info_dlg.vbox.pack_start(vbox)
            box1, box2 = gu.hig_category_vbox(_("File locations"))
            vbox.pack_start(box1)
            sizegroup = gtk.SizeGroup(gtk.SIZE_GROUP_HORIZONTAL)
            # statistics.sqlite
            # win32 solfegerc
            # win32 langenviron.txt
            box2.pack_start(gu.hig_label_widget(_("Solfege application data:"), gtk.Label(filesystem.app_data()), sizegroup))
            box2.pack_start(gu.hig_label_widget(_("Solfege user data:"), gtk.Label(filesystem.user_data()), sizegroup))
            box2.pack_start(gu.hig_label_widget(_("Solfege config file:"), gtk.Label(filesystem.rcfile()), sizegroup))
            box2.pack_start(gu.hig_label_widget(_("Solfege installation directory:"), gtk.Label(os.getcwdu()), sizegroup))
            box2.pack_start(gu.hig_label_widget(_("User manual in HTML format:"), gtk.Label(os.path.join(os.getcwdu(), "help")), sizegroup))
            self.g_path_info_dlg.show_all()
            def f(*w):
                self.g_path_info_dlg.hide()
                return True
            self.g_path_info_dlg.connect('response', f)
            self.g_path_info_dlg.connect('delete-event', f)
        else:
            self.g_path_info_dlg.show()
    def show_about_window(self, widget):
        trans = _("SOLFEGETRANSLATORS")
        if trans == 'SOLFEGETRANSLATORS':
            trans = ""
        pixbuf = self.render_icon('solfege-icon', gtk.ICON_SIZE_DIALOG)
        a = self.g_about_window = gtk.AboutDialog()
        a.set_logo(pixbuf)
        a.set_name("GNU Solfege")
        a.set_website("http://www.solfege.org")
        a.set_version(buildinfo.VERSION_STRING)
        a.set_copyright("\n".join((solfege_copyright, warranty)))
        a.set_authors(["Tom Cato Amundsen",
              'Giovanni Chierico %s' % _("(some lessonfiles)"),
              'Michael Becker %s' % _("(some lessonfiles)"),
              'Joe Lee %s' % _("(sound code for the MS Windows port)"),
              'Steve Lee %s' % _("(ported winmidi.c to gcc)"),
              'Thibaus Cousin %s' % _("(spec file for SuSE 8.2)"),
              'David Coe %s' %_("(spec file cleanup)"),
              'David Petrou %s' % _("(testing and portability fixes for FreeBSD)"),
              'Han-Wen Nienhuys %s' % _("(the music font from Lilypond)"),
              'Jan Nieuwenhuizen %s' % _("(the music font from Lilypond)"),
              'Davide Bonetti %s' % _("(scale exercises)"),
              ])
        a.set_documenters(["Tom Cato Amundsen",
                "Tom Eykens",
                ])
        a.set_translator_credits(_("SOLFEGETRANSLATORS"))
        self.g_about_window.run()
        self.g_about_window.destroy()
    def do_tree_editor(self, *v):
        """
        Open a learning tree editor editing the current front page.
        """
        fpeditor.Editor.edit_file(self.get_string("app/frontpage"))
    def post_constructor(self):
        global splash_win
        self.m_frontpage_merge_id = None
        self.create_frontpage_menu()
        self.g_ui_manager.add_ui_from_file("help-menu.xml")
        if solfege.app.m_sound_init_exception is not None:
            if splash_win:
                splash_win.destroy()
                splash_win = None
            solfege.app.display_sound_init_error_message(solfege.app.m_sound_init_exception)
        # MIGRATION 3.9.0
        if sys.platform == "win32" \
            and os.path.exists(os.path.join(filesystem.get_home_dir(), "lessonfiles")) \
            and not os.path.exists(filesystem.user_lessonfiles()):
                if splash_win:
                    splash_win.hide()
                do_move = gu.dialog_yesno(_('In Solfege 3.9.0, the location where Solfege look for lesson files you have created was changed. The files has to be moved from "%(old)s" and into the folder "%(gnu)s" in your "%(doc)s" folder.\nMay I move the files automatically for you now?' % {
                    'doc':  os.path.split(os.path.split(filesystem.user_data())[0])[1],
                    'gnu':  os.path.join(filesystem.appname, 'lessonfiles'),
                    'old': os.path.join(filesystem.get_home_dir(), "lessonfiles"),
                  }), parent=self)
                if do_move:
                    try:
                        os.makedirs(filesystem.user_data())
                        shutil.copytree(os.path.join(filesystem.get_home_dir(), "lessonfiles"),
                                        os.path.join(filesystem.user_data(), "lessonfiles"))
                    except (OSError, shutil.Error), e:
                        gu.dialog_ok(_("Error while copying directory:\n%s" % e))
                    else:
                        gu.dialog_ok(_("Files copied. The old files has been left behind. Please delete them when you have verified that all files was copied correctly."))

                if splash_win:
                    splash_win.show()
        # MIGRATION 3.9.3 when we added langenviron.bat and in 3.11
        # we migrated to langenviron.txt because we does not use cmd.exe
        if sys.platform == 'win32' and winlang.win32_get_langenviron() != self.get_string('app/lc_messages'):
            gu.dialog_ok(_("Migrated old language setup. You might have to restart the program all translated messages to show up."))
            winlang.win32_put_langenviron(self.get_string('app/lc_messages'))
        # MIGRATION 3.11.1: earlier editors would create new learning trees
        # below app_data() instead of user_data().
        if (sys.platform == "win32" and 
            os.path.exists(os.path.join(filesystem.app_data(),
                                        "learningtrees"))):
            if not os.path.exists(os.path.join(filesystem.user_data(), "learningtrees")):
                os.makedirs(os.path.join(filesystem.user_data(), "learningtrees"))
            for fn in os.listdir(os.path.join(filesystem.app_data(), "learningtrees")):
                if not os.path.exists(os.path.join(filesystem.user_data(), "learningtrees", fn)):
                    shutil.move(os.path.join(filesystem.app_data(), "learningtrees", fn),
                            os.path.join(filesystem.user_data(), "learningtrees"))
                else:
                    # We add the .bak exstention if the file already exists.
                    shutil.move(os.path.join(filesystem.app_data(), "learningtrees", fn),
                            os.path.join(filesystem.user_data(), "learningtrees", u"%s.bak" % fn))
                os.rmdir(os.path.join(os.path.join(filesystem.app_data(), "learningtrees")))
        item = self.g_ui_manager.get_widget("/Menubar/FileMenu/FrontPagesMenu")
        item.connect('activate', lambda s: self.create_frontpage_menu())
        try:
            i18n.locale_setup_failed
            print >> sys.stderr, "\n".join(textwrap.wrap("Translations are disabled because your locale settings are broken. This is not a bug in GNU Solfege, so don't report it. The README file distributed with the program has some more details."))
        except AttributeError:
            pass
    def activate_exercise(self, module, urlobj=None):
        self.show_view(module)
        # We need this test because not all exercises use a notebook.
        if self.get_view().g_notebook:
            if urlobj and urlobj.action in ['practise', 'config', 'statistics']:
                self.get_view().g_notebook.set_current_page(
                   ['practise', 'config', 'statistics'].index(urlobj.action))
            else:
                self.get_view().g_notebook.set_current_page(0)
        self.set_title("Solfege - " + self.get_view().m_t.m_P.header.title)
    def display_docfile(self, fn, anchor):
        """
        Display the HTML file named by fn in the help browser window.
        """
        for lang in solfege.app.m_userman_language, "C":
            filename = os.path.join(os.getcwdu(), u"help", lang, fn)
            if os.path.isfile(filename):
                break
        try:
            webbrowser.open(filename)
        except Exception, e:
            self.display_error_message2(_("Error opening web browser"), str(e))
    def display_user_exercises(self, w):
        col = frontpage.Column()
        page = frontpage.Page(_('User exercises'), col)
        curdir =None
        for filename in lessonfile.infocache.iter_user_files():
            dir, fn = os.path.split(filename)
            if dir != curdir:
                curdir = dir
                linklist = frontpage.LinkList(dir)
                col.append(linklist)
            linklist.append(filename)
        # Move ~/.solfege/exercises/user/lesson-files first in the list
        # since we refer to this directory in the user manual
        for idx, c in enumerate(col):
            if c[0].startswith(os.path.join(filesystem.user_data(), u"exercises", u"user", u"lesson-files")):
                col.insert(0, c)
                del col[idx+1]
                break
        else:
            linklist = frontpage.LinkList(os.path.join(filesystem.user_data(), u"exercises", u"user", u"lesson-files"))
            col.insert(0, linklist)
            
        if os.path.isdir(filesystem.user_lessonfiles()):
            linklist = None
            # Added just to be nice with people not moving their files from
            # pre 3.15.3 location:
            for filename in os.listdir(filesystem.user_lessonfiles()):
                if not linklist:
                    linklist = frontpage.LinkList(filesystem.user_lessonfiles())
                linklist.append(os.path.join(filesystem.user_lessonfiles(), filename))
            # only display the linklist if there are any files.
            if linklist:
                col.append(linklist)
        self.display_frontpage(page)
    def display_recent_exercises(self, w):
        data = frontpage.Page(_('Recent exercises'),
            [frontpage.Column(
                [frontpage.LinkList(_('Recent exercises'),
                   solfege.db.recent(8))])])
        self.display_frontpage(data, show_topics=True)
        self.get_view().g_searchbox.hide()
    def display_testpage(self, widget=None):
        """
        Display the front page of the data  in solfege.app.m_frontpage_data
        """
        self.set_title("GNU Solfege - tests")
        if not self.show_view('testspage'):
            self.add_view(TestsView(), 'testspage')
        self.get_view().g_searchbox.show()
        self.get_view().display_data(solfege.app.m_frontpage_data)
    def on_search_all_exercises(self, widget=None):
        self.set_title("GNU Solfege")
        if not self.show_view('searchview'):
            self.add_view(SearchView(), 'searchview')
    def display_frontpage(self, data=None, show_topics=False):
        """
        Display the front page of the data  in solfege.app.m_frontpage_data
        """
        self.set_title("GNU Solfege")
        if not self.show_view('frontpage'):
            self.add_view(FrontPage(), 'frontpage')
        self.get_view().g_searchbox.show()
        if not data:
            data = solfege.app.m_frontpage_data
        self.get_view().display_data(data, show_topics=show_topics)
    def initialise_exercise(self, teacher):
        """
        Create a Gui object for the exercise and add it to
        the box_dict dict.
        """
        assert teacher.m_exname not in self.box_dict
        self.get_view().hide()
        m = __import__("solfege.exercises.%s" % teacher.m_exname, fromlist=("solfege.exercises.%s" % teacher.m_exname,), level=0)
        self.add_view(m.Gui(teacher), teacher.m_exname)
    def on_key_press_event(self, widget, event):
        self.get_view().on_key_press_event(widget, event)
    def open_preferences_window(self, widget=None):
        if not self.g_config_window:
            self.g_config_window = ConfigWindow()
            self.g_config_window.show()
        else:
            self.g_config_window.show()
    def quit_program(self, *w):
        can_quit = True
        for dlg in gu.EditorDialogBase.instance_dict.values():
            if dlg.close_window():
                dlg.destroy()
            else:
                can_quit = False
                break
        if can_quit:
            solfege.app.quit_program()
            gtk.main_quit()
        else:
            return True
    def display_in_musicviewer(self, music):
        if not self.g_musicviewer_window:
            self.g_musicviewer_window = MusicViewerWindow()
            self.g_musicviewer_window.show()
        self.g_musicviewer_window.display_music(music)
    def close_musicviewer(self, widget=None):
        self.g_musicviewer_window.destroy()
        self.g_musicviewer_window = None
    def enter_test_mode(self):
        if 'enter_test_mode' not in dir(self.get_view()):
            gu.dialog_ok(_("The '%s' exercise module does not support test yet." % self.m_viewer))
            return
        self.m_action_groups['NotExit'].set_sensitive(False)
        self.g = self.get_view().g_notebook.get_nth_page(0)
        self.get_view().g_notebook.get_nth_page(0).reparent(self.main_box)
        self.get_view().g_notebook.hide()
        self.get_view().enter_test_mode()
    def exit_test_mode(self):
        solfege.app.m_test_mode = False
        self.m_action_groups['NotExit'].set_sensitive(True)
        box = gtk.VBox()
        self.get_view().g_notebook.insert_page(box, gtk.Label(_("Practise")), 0)
        self.g.reparent(box)
        self.get_view().g_notebook.show()
        self.get_view().g_notebook.get_nth_page(0).show()
        self.get_view().g_notebook.set_current_page(0)
        self.get_view().exit_test_mode()
    def new_training_set_editor(self, widget):
        dlg = TrainingSetDialog()
        dlg.show_all()
    def new_practisesheet_editor(self, widget):
        dlg = PractiseSheetDialog()
        dlg.show_all()

import locale
locale.setlocale(locale.LC_NUMERIC, "C")

# check_rcfile has to be called before and
# functions that use the cfg module.
application.check_rcfile()

cfg.set_bool('config/no_random', bool(options.no_random))

lessonfile.infocache = lessonfile.InfoCache()

if splash_win:
    if not os.path.exists(statistics.DB.get_statistics_filename()):
        splash_win.show_progress(_("Importing old statistics"))
def f(s):
    if splash_win:
        splash_win.show_progress(s)
solfege.db = statistics.DB(f)

if splash_win:
    splash_win.show_progress(_("Creating application window"))

def start_app(datadir):
    global splash_win
    solfege.app = application.SolfegeApp(options)
    solfege.win = w = MainWin(options, datadir)
    solfege.app.setup_sound()
    w.show()
    w.post_constructor()
    if splash_win:
        splash_win.destroy()
        splash_win = None

    def ef(t, value, traceback):
        if options.debug:
            msg = "ehooked:" + str(value)
        else:
            msg = str(value)
        if issubclass(t, lessonfile.LessonfileException):
            w.display_error_message(msg, str(t))
        elif issubclass(t, osutils.ExecutableDoesNotExist):
            if len(value.args) > 1:
                w.display_error_message2(value.args[0], "\n".join(value.args[1:]))
            else:
                w.display_error_message(msg, str(t))
        else:
            sys.__excepthook__(t, msg, traceback)
    if not options.disable_exception_handler:
        sys.excepthook = ef
    print time.time() - start_time
    # We parse all lesson files when we are idle to save a half a
    # second the first time the user searches all lesson files using
    # Ctrl-F.
    lessonfile.infocache.parse_all_files(True)
    gtk.main()

