# GNU Solfege - free ear training software # Copyright (C) 2000, 2001, 2002, 2003, 2004, 2005, 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 gu from specialwidgets import QuestionNameCheckButtonTable import soundcard, mpd, mpd.musicdisplayer import abstract, const, lessonfile import utils import soundcard class Teacher(abstract.Teacher): OK = 0 ERR_PICKY = 1 # valid values for self.q_status: # QSTATUS_NO at program startup # QSTATUS_NEW after the new button has been pressed # QSTATUS_SOLVED when all three questions have been answered # QSTATUS_GIVE_UP after 'Give Up' has been pressed. CORRECT = 1 ALL_CORRECT = 2 def __init__(self, exname, app): abstract.Teacher.__init__(self, exname, app) self.lessonfileclass = lessonfile.ChordLessonfile def new_question(self): """ return OK or ERR_PICKY UI will never call this function unless we have a usable lessonfile. """ assert self.m_P if self.get_bool('config/picky_on_new_question') \ and (not self.q_status in (const.QSTATUS_NO, const.QSTATUS_SOLVED, const.QSTATUS_GIVE_UP)): return self.ERR_PICKY self.m_P.select_random_question() self.m_correct = [self.m_P.get_cname() == -1, self.m_P.get_inversion() == -1, self.m_P.get_toptone() == -1] self.q_status = const.QSTATUS_NEW return self.OK def give_up(self): self.q_status = const.QSTATUS_GIVE_UP def guess_type(self, t): """ GUI guarantees that this method will not be called after it has been guessed correct once. return 0 if this was wrong guess. return CORRECT if this question is correct. return ALL_CORRECT if all parts of the question is correct. """ assert self.q_status == const.QSTATUS_NEW if t == self.m_P.get_cname(): self.m_correct[0] = 1 if self.m_correct == [1, 1, 1]: self.q_status = const.QSTATUS_SOLVED return self.ALL_CORRECT return self.CORRECT else: return 0 def guess_inversion(self, t): """ GUI guarantees that this method will not be called after it has been guessed correct once. return 0 if this was wrong guess. return CORRECT if this question is correct. return ALL_CORRECT if all parts of the question is correct. """ assert self.q_status == const.QSTATUS_NEW if t == self.m_P.get_inversion(): self.m_correct[1] = 1 if self.m_correct == [1, 1, 1]: self.q_status = const.QSTATUS_SOLVED return self.ALL_CORRECT return self.CORRECT else: return 0 self.m_correct[1] = 1 return t == self.m_P.get_inversion() def guess_toptone(self, t): """ GUI guarantees that this method will not be called after it has been guessed correct once. return 0 if this was wrong guess. return CORRECT if this question is correct. return ALL_CORRECT if all parts of the question is correct. """ assert self.q_status == const.QSTATUS_NEW if t == self.m_P.get_toptone(): self.m_correct[2] = 1 if self.m_correct == [1, 1, 1]: self.q_status = const.QSTATUS_SOLVED return self.ALL_CORRECT return self.CORRECT else: return 0 self.m_correct[2] = 1 return t == self.m_P.get_toptone() class Gui(abstract.LessonbasedGui): def __init__(self, teacher, window): abstract.LessonbasedGui.__init__(self, teacher, window) ################ # practise_box # ################ hbox = gu.bHBox(self.practise_box) hbox.set_spacing(gu.PAD) spacebox = gtk.HBox() hbox.pack_start(spacebox) table = gtk.Table(4, 5, 0) hbox.pack_start(table, False) hsep = gtk.HSeparator() table.attach(hsep, 0, 5, 1, 2, xoptions=gtk.FILL, yoptions=gtk.FILL, xpadding=0, ypadding=gu.PAD_SMALL) self.g_vsep1 = gtk.VSeparator() table.attach(self.g_vsep1, 1, 2, 0, 4, xoptions=0, xpadding=gu.PAD) self.g_vsep2 = gtk.VSeparator() table.attach(self.g_vsep2, 3, 4, 0, 4, xoptions=0, xpadding=gu.PAD) table.attach(gtk.Label(_("Chord type")), 0, 1, 0, 1, xoptions=gtk.FILL, yoptions=0) self.g_chordtype_box = gtk.VBox() table.attach(self.g_chordtype_box, 0, 1, 2, 3, xoptions=gtk.FILL) self.g_inversion_label = gtk.Label(_("Inversion")) table.attach(self.g_inversion_label, 2, 3, 0, 1, xoptions=gtk.FILL, yoptions=0) self.g_inversion_box = gtk.VBox() table.attach(self.g_inversion_box, 2, 3, 2, 3, xoptions=gtk.FILL) self.g_toptone_label = gtk.Label(_("Toptone")) table.attach(self.g_toptone_label, 4, 5, 0, 1, xoptions=gtk.FILL, yoptions=0) self.g_toptone_box = gtk.VBox() table.attach(self.g_toptone_box, 4, 5, 2, 3, xoptions=gtk.FILL) self.g_music_displayer = mpd.musicdisplayer.MusicDisplayer(utils.play_tone) self.g_music_displayer.set_size_request(100, -1) hbox.pack_start(self.g_music_displayer, False) spacebox = gtk.HBox() hbox.pack_start(spacebox) self.g_flashbar = gu.FlashBar() self.practise_box.pack_start(self.g_flashbar, False) self.g_new = gu.bButton(self.action_area, _("_New chord"), self.new_question) self.g_repeat = gu.bButton(self.action_area, _("_Repeat"), lambda w: self.run_exception_handled(self.m_t.m_P.play_question)) self.g_repeat_arpeggio = gu.bButton(self.action_area, _("Repeat _arpeggio"), lambda w: self.run_exception_handled(self.m_t.m_P.play_question_arpeggio)) self.g_give_up = gu.bButton(self.action_area, _("_Give up"), self.give_up) self.practise_box.show_all() ############## # config_box # ############## self.config_box.set_spacing(gu.PAD_SMALL) self.add_random_transpose_gui() # ----------------------------------------- self.g_select_questions_category_box, category_box= gu.hig_category_vbox( _("Chord types to ask")) self.config_box.pack_start(self.g_select_questions_category_box, True) self.g_select_questions = QuestionNameCheckButtonTable(self.m_t) self.g_select_questions.initialize(4, 0) category_box.pack_start(self.g_select_questions, False) self.g_select_questions.show() def update_select_question_buttons(self): if self.m_t.m_custom_mode: self.g_select_questions_category_box.show() self.g_select_questions.initialize(self.m_t.m_P.header.fillnum, self.m_t.m_P.header.filldir) self.m_t.check_askfor() for question in self.m_t.m_P.iterate_questions_with_unique_names(): self.g_select_questions.add(question, 'normal') else: self.g_select_questions_category_box.hide() self.g_select_questions.initialize(0, 0) def update_answer_buttons(self, obj=None): for x in self.g_chordtype_box.get_children() \ + self.g_inversion_box.get_children() \ + self.g_toptone_box.get_children(): x.destroy() if self.m_t.m_P: for ctype in self.m_t.m_P.get_unique_cnames(): translated_type = self.m_t.m_P.m_chord_types[ctype] b = gtk.Button(translated_type) sr = b.size_request() b.set_size_request(int(sr[0]*1.3), sr[1]) b.set_data('type', ctype) b.connect('clicked', self.on_type) b.connect('button_release_event', self.on_type) self.g_chordtype_box.pack_start(b, False) b.show() v = self.m_t.m_P.m_inversions v.sort() for x in v: if x == 0: s = _("root position") else: s = _("%i. inversion") % x b = gtk.Button(s) sr = b.size_request() b.set_size_request(int(sr[0]*1.3), sr[1]) b.set_data('inversion', x) b.connect('clicked', self.on_inversion) b.connect('button_release_event', self.on_inversion) self.g_inversion_box.pack_start(b, False) b.show() v = self.m_t.m_P.m_toptones v.sort() for x in v: b = gtk.Button(str(x)) b.set_data('toptone', x) b.connect('clicked', self.on_toptone) b.connect('button_release_event', self.on_toptone) self.g_toptone_box.pack_start(b, False) b.show() self.g_inversion_box.set_sensitive(True) self.g_toptone_box.set_sensitive(True) def update_gui_after_lessonfile_change(self): self.g_music_displayer.clear() self.update_select_question_buttons() if self.m_t.m_P.header.lesson_heading: self.set_lesson_heading(self.m_t.m_P.header.lesson_heading) else: self.set_lesson_heading(_("Identify the chord")) self.g_new.set_sensitive(True) self.g_random_transpose.set_text(str(self.m_t.m_P.header.random_transpose)) if not [q for q in self.m_t.m_P.m_questions if 'toptone' in q]: self.g_toptone_label.hide() self.g_vsep2.hide() else: self.g_toptone_label.show() self.g_vsep2.show() if not [q for q in self.m_t.m_P.m_questions if 'inversion' in q]: self.g_inversion_label.hide() self.g_vsep1.hide() else: self.g_inversion_label.show() self.g_vsep1.show() self.g_repeat.set_sensitive(False) self.g_repeat_arpeggio.set_sensitive(False) self.g_give_up.set_sensitive(False) self.update_answer_buttons() def on_type(self, button, event=None): if event is None: self.on_type_left_clicked(button) elif event.button == 3: if self.m_t.m_P and self.m_t.m_P.header.enable_right_click: self.on_type_right_clicked(button) def on_type_left_clicked(self, button): if self.m_t.q_status in (const.QSTATUS_NO, const.QSTATUS_GIVE_UP)\ or self.m_t.m_correct[0]: return g = self.m_t.guess_type(button.get_data('type')) if g: self.g_flashbar.flash(_("Correct")) for b in self.g_chordtype_box.get_children(): if b == button: b.get_children()[0].set_name("BoldText") if g == self.m_t.ALL_CORRECT: self.all_guessed_correct() else: self.g_flashbar.flash(_("Wrong")) if self.m_t.m_correct == [1, 1, 1]: self.run_exception_handled(self.show_answer) self.g_new.set_sensitive(True) self.g_new.grab_focus() self.g_give_up.set_sensitive(False) def on_type_right_clicked(self, button): if self.m_t.q_status == const.QSTATUS_NO: return if not self.m_t.m_P.has_question(): return if self.m_t.m_P.get_toptone() == -1 \ and self.m_t.m_P.get_inversion() == -1: if 'set' in self.m_t.m_P.get_question(): for question in self.m_t.m_P.m_questions: if question['set'] == self.m_t.m_P.get_question()['set'] \ and question.get_cname() == button.get_data('type'): self.run_exception_handled(self.m_t.m_P.play_question, question) return else: for question in self.m_t.m_P.m_questions: if question.get_cname() == button.get_data('type'): self.run_exception_handled(self.m_t.m_P.play_question, question) return else: #Try for exact match for question in self.m_t.m_P.m_questions: if question.get_toptone() != self.m_t.m_P.get_toptone(): continue if question.get_inversion() != self.m_t.m_P.get_inversion(): continue if question.get_cname() == button.get_data('type'): self.run_exception_handled(self.m_t.m_P.play_question, question) return # try to match, ignoring toptone for question in self.m_t.m_P.m_questions: if question.get_inversion() != self.m_t.m_P.get_inversion(): continue if question.get_cname() == button.get_data('type'): self.run_exception_handled(self.m_t.m_P.play_question, question) return # match if only type matches. for question in self.m_t.m_P.m_questions: if question.get_cname() == button.get_data('type'): self.run_exception_handled(self.m_t.m_P.play_question, question) return def on_inversion(self, button, event=None): if event is None: self.on_inversion_left_clicked(button) elif event.button == 3: if self.m_t.m_P and self.m_t.m_P.header.enable_right_click: self.on_inversion_right_clicked(button) def on_inversion_left_clicked(self, button): if self.m_t.q_status in (const.QSTATUS_NO, const.QSTATUS_GIVE_UP) \ or self.m_t.m_correct[1]: return if self.m_t.guess_inversion(button.get_data('inversion')): self.g_flashbar.flash(_("Correct")) for b in self.g_inversion_box.get_children(): if b == button: b.get_children()[0].set_name("BoldText") else: self.g_flashbar.flash(_("Wrong")) if self.m_t.m_correct == [1, 1, 1]: self.run_exception_handled(self.show_answer) self.g_new.set_sensitive(True) self.g_new.grab_focus() self.g_give_up.set_sensitive(False) def on_inversion_right_clicked(self, button): """ First we try to find a chord with the same type and toptone as the question, and with toptone as in button.get_data('toptone'). Second best is to ignore inversion and find a chord where chord type is the same as the questions, and with toptone as button.get_data('toptone') """ if not self.m_t.m_P.has_question(): return # first we try to get an exact match for question in self.m_t.m_P.m_questions: if question.get_toptone() != -1 \ and question.get_toptone() != self.m_t.m_P.get_toptone(): continue if question.get_cname() == self.m_t.m_P.get_cname() \ and question.get_inversion() == button.get_data('inversion'): self.run_exception_handled(self.m_t.m_P.play_question, question) return # then we tries to match chord type and inversion, ignoring toptone for question in self.m_t.m_P.m_questions: if question.get_cname() == self.m_t.m_P.get_question().get_cname()\ and question.get_inversion() == button.get_data('inversion'): self.run_exception_handled(self.m_t.m_P.play_question, question) return def on_toptone(self, button, event=None): if not event: self.on_toptone_left_clicked(button) elif event.button == 3: if self.m_t.m_P and self.m_t.m_P.header.enable_right_click: self.on_toptone_right_clicked(button) def on_toptone_right_clicked(self, button): if not self.m_t.m_P.has_question(): return # first we try to get an exact match for question in self.m_t.m_P.m_questions: if question.get_inversion() != -1 \ and question.get_inversion() != self.m_t.m_P.get_inversion(): continue if question.get_cname() == self.m_t.m_P.get_cname() \ and question.get_toptone() == button.get_data('toptone'): self.run_exception_handled(self.m_t.m_P.play_question, question) return # then we tries to match chord type and toptone, ignoring inversion for question in self.m_t.m_P.m_questions: if question.get_cname() == self.m_t.m_P.get_question().get_cname()\ and question.get_toptone() == button.get_data('toptone'): sel.run_exception_handled(self.m_t.m_P.play_question, question) return def on_toptone_left_clicked(self, button): if self.m_t.q_status in (const.QSTATUS_NO, const.QSTATUS_GIVE_UP) \ or self.m_t.m_correct[2]: return if self.m_t.guess_toptone(button.get_data('toptone')): self.g_flashbar.flash(_("Correct")) for b in self.g_toptone_box.get_children(): if b == button: b.get_children()[0].set_name("BoldText") else: self.g_flashbar.flash(_("Wrong")) if self.m_t.m_correct == [1, 1, 1]: self.run_exception_handled(self.show_answer) self.g_new.set_sensitive(True) self.g_new.grab_focus() self.g_give_up.set_sensitive(False) def all_guessed_correct(self): self.g_new.set_sensitive(True) self.g_give_up.set_sensitive(False) def new_question(self, widget=None): def exception_cleanup(): soundcard.synth.stop() self.g_give_up.set_sensitive(False) self.g_repeat.set_sensitive(False) self.g_repeat_arpeggio.set_sensitive(False) self.m_t.q_status = const.QSTATUS_NO # if we have no lessonfile, then we have to questions. if not self.m_t.m_P: return # make sure all buttons are sensitive. for button in self.g_chordtype_box.get_children() \ + self.g_inversion_box.get_children() \ + self.g_toptone_box.get_children(): button.get_children()[0].set_name("") ## try: n = self.m_t.new_question() if n == Teacher.ERR_PICKY: print "picky!" else: self.g_music_displayer.clear() self.m_t.m_P.play_question() self.g_give_up.set_sensitive(True) self.g_repeat.set_sensitive(True) if self.get_bool('config/picky_on_new_question'): self.g_new.set_sensitive(False) self.g_repeat_arpeggio.set_sensitive(True) self.g_inversion_box.set_sensitive(self.m_t.m_P.get_inversion() != -1) self.g_toptone_box.set_sensitive(self.m_t.m_P.get_toptone() != -1) self.g_chordtype_box.get_children()[0].grab_focus() except Exception, e: if not self.standard_exception_handler(e, __file__, exception_cleanup): raise def on_start_practise(self): self.m_t.m_custom_mode = self.get_bool('gui/expert_mode') for question in self.m_t.m_P.m_questions: question['active'] = 1 self.update_gui_after_lessonfile_change() self.g_flashbar.require_size([ _("Click 'New chord' to begin."), "XXXX, root position, toptone: 5", ]) self.g_new.grab_focus() gobject.timeout_add(const.SHORT_WAIT, lambda self=self: self.g_flashbar.flash(_("Click 'New chord' to begin."))) def on_end_practise(self): self.m_t.end_practise() self.g_music_displayer.clear() self.g_new.set_sensitive(True) self.g_repeat.set_sensitive(False) self.g_repeat_arpeggio.set_sensitive(False) self.g_give_up.set_sensitive(False) def give_up(self, widget=None): if self.m_t.q_status == const.QSTATUS_NEW: self.m_t.give_up() self.run_exception_handled(self.show_answer) type_txt = self.m_t.m_P.m_chord_types[self.m_t.m_P.get_cname()] i = self.m_t.m_P.get_inversion() if i == 0: inversion_txt = _("root position") elif i > 0: inversion_txt = _("%i. inversion") % i else: inversion_txt = "" i = self.m_t.m_P.get_toptone() if i != -1: toptone_txt = "toptone: %i" % i else: toptone_txt = "" v = [type_txt] if inversion_txt: v.append(inversion_txt) if toptone_txt: v.append(toptone_txt) if len(v) == 3: self.g_flashbar.flash(_("%(one)s, %(two)s and %(three)s") % {'one': v[0], 'two': v[1], 'three': v[2]}) elif len(v) == 2: self.g_flashbar.flash(_("%(one)s and %(two)s") % {'one': v[0], 'two': v[1]}) else: self.g_flashbar.flash(type_txt) self.g_new.set_sensitive(True) self.g_give_up.set_sensitive(False) for button in self.g_chordtype_box.get_children(): if button.get_data('type') == self.m_t.m_P.get_cname(): button.get_children()[0].set_name('BoldText') else: button.get_children()[0].set_name('') for button in self.g_inversion_box.get_children(): if button.get_data('inversion') == self.m_t.m_P.get_inversion(): button.get_children()[0].set_name('BoldText') else: button.get_children()[0].set_name('') for button in self.g_toptone_box.get_children(): if button.get_data('toptone') == self.m_t.m_P.get_toptone(): button.get_children()[0].set_name('BoldText') else: button.get_children()[0].set_name('') def show_answer(self): """ Show the answer in the music displayer. All callers must check for exceptions. """ fontsize = self.get_int('config/feta_font_size=20') if isinstance(self.m_t.m_P.get_question()['music'], lessonfile.Chord): clef = mpd.select_clef(self.m_t.m_P.get_music_as_notename_string('music')) self.g_music_displayer.display(r"\staff{\clef %s <%s>}" % (clef, self.m_t.m_P.get_music_as_notename_string('music')), fontsize) else: self.g_music_displayer.display(self.m_t.m_P.get_music(), fontsize)