~ubuntu-branches/ubuntu/saucy/solfege/saucy

« back to all changes in this revision

Viewing changes to solfege/abstract.py

  • Committer: Bazaar Package Importer
  • Author(s): Tom Cato Amundsen
  • Date: 2010-03-28 06:34:28 UTC
  • mfrom: (1.1.10 upstream) (2.1.7 sid)
  • Revision ID: james.westby@ubuntu.com-20100328063428-wg2bqvoce2aq4xfb
Tags: 3.15.9-1
* New upstream release.
* Redo packaging. 

Show diffs side-by-side

added added

removed removed

Lines of Context:
 
1
# vim: set fileencoding=utf-8 :
 
2
# GNU Solfege - free ear training software
 
3
# Copyright (C) 2000, 2001, 2002, 2003, 2004, 2005, 2006, 2007, 2008  Tom Cato Amundsen
 
4
#
 
5
# This program is free software: you can redistribute it and/or modify
 
6
# it under the terms of the GNU General Public License as published by
 
7
# the Free Software Foundation, either version 3 of the License, or
 
8
# (at your option) any later version.
 
9
#
 
10
# This program is distributed in the hope that it will be useful,
 
11
# but WITHOUT ANY WARRANTY; without even the implied warranty of
 
12
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 
13
# GNU General Public License for more details.
 
14
#
 
15
# You should have received a copy of the GNU General Public License
 
16
# along with this program.  If not, see <http://www.gnu.org/licenses/>.
 
17
 
 
18
from __future__ import absolute_import
 
19
 
 
20
import random
 
21
import sys
 
22
import traceback
 
23
 
 
24
import gtk, gobject
 
25
 
 
26
from solfege import gu
 
27
from solfege import inputwidgets
 
28
from solfege.specialwidgets import RandomTransposeDialog
 
29
from solfege.exceptiondialog import ExceptionDialog
 
30
from solfege import soundcard, mpd
 
31
from solfege import utils
 
32
from solfege import cfg, lessonfile, osutils, const
 
33
 
 
34
import solfege
 
35
 
 
36
class QstatusDefs:
 
37
    QSTATUS_NO = 0
 
38
    QSTATUS_NEW = 1
 
39
    QSTATUS_WRONG = 2
 
40
    QSTATUS_SOLVED = 3
 
41
    QSTATUS_GIVE_UP = 4
 
42
    QSTATUS_VOICING_SOLVED = 5
 
43
    QSTATUS_VOICING_WRONG = 6
 
44
    QSTATUS_TYPE_WRONG = 7
 
45
    QSTATUS_TYPE_SOLVED = 8
 
46
 
 
47
class Teacher(cfg.ConfigUtils, QstatusDefs):
 
48
    def __init__(self, exname):
 
49
        cfg.ConfigUtils.__init__(self, exname)
 
50
        self.q_status = self.QSTATUS_NO
 
51
        self.m_statistics = None
 
52
        self.m_timeout_handle = None
 
53
        self.m_P = None
 
54
        # The file name, not a file object
 
55
        self.m_lessonfile = None
 
56
        self.m_question = None
 
57
    def maybe_auto_new_question(self):
 
58
        if self.get_bool('new_question_automatically'):
 
59
            if self.m_timeout_handle is None:
 
60
                def remove_timeout(self=self):
 
61
                    self.m_timeout_handle = None
 
62
                    self.g_view.new_question()
 
63
                self.m_timeout_handle = gobject.timeout_add(int(self.get_float('seconds_before_new_question')*1000),  remove_timeout)
 
64
    def end_practise(self):
 
65
        if self.m_timeout_handle:
 
66
            gobject.source_remove(self.m_timeout_handle)
 
67
            self.m_timeout_handle = None
 
68
        self.q_status = self.QSTATUS_NO
 
69
        soundcard.synth.stop()
 
70
    def exit_test_mode(self):
 
71
        """
 
72
        Shared between harmonic and melodic interval.
 
73
        """
 
74
        self.m_statistics.exit_test_mode()
 
75
    def set_lessonfile(self, lessonfile):
 
76
        """
 
77
        Set the variable 'm_lessonfile' and
 
78
        parse the lesson file and save the statistics.
 
79
        """
 
80
        self.m_lessonfile = lessonfile
 
81
        self.parse_lessonfile()
 
82
        if self.m_P and self.m_statistics:
 
83
            solfege.db.validate_stored_statistics(self.m_P.m_filename)
 
84
    def parse_lessonfile(self):
 
85
        self.m_question = None
 
86
        self.q_status = self.QSTATUS_NO
 
87
        if not self.m_lessonfile:
 
88
            self.m_P = None
 
89
            return
 
90
        self.m_P = self.lessonfileclass()
 
91
        try:
 
92
            self.m_P.parse_file(self.m_lessonfile)
 
93
        # We don't have to check for LessonfileExceptions here because
 
94
        # the global exception hook will catch them.
 
95
        except IOError, e:
 
96
            self.m_P = None
 
97
            solfege.win.display_error_message(
 
98
"""There was an IOError while trying to parse a lesson file:
 
99
%s.""" % e)
 
100
            return
 
101
        if [q for q in self.m_P.m_questions if isinstance(q['music'], lessonfile.Cmdline)]:
 
102
            run = gu.dialog_yesno(_("The lessonfile contain potentially dangerous code because it run external programs. Run anyway?"))
 
103
            if not run:
 
104
                self.m_P = None
 
105
    def check_askfor(self):
 
106
        if self.m_custom_mode:
 
107
            self.set_list('ask_for_names', range(len(self.m_P.get_unique_cnames())))
 
108
    def play_tonic(self):
 
109
        """
 
110
        Play the tonic of the question, if defined.
 
111
        """
 
112
        if 'tonic' in self.m_P.get_question():
 
113
            self.m_P.play_question(None, 'tonic')
 
114
 
 
115
class MelodicIntervalTeacher(Teacher):
 
116
    """
 
117
    Base class for interval exercises where
 
118
    you can have more than one interval.
 
119
    When this class was created it was used by melodic-intevall
 
120
    and sing-interval.
 
121
    """
 
122
    OK = 0
 
123
    ERR_PICKY = 1
 
124
    ERR_CONFIGURE = 2
 
125
    def __init__(self, exname):
 
126
        Teacher.__init__(self, exname)
 
127
        self.m_tonika = None
 
128
        self.m_question = []
 
129
    def new_question(self, L, H):
 
130
        assert isinstance(L, basestring)
 
131
        assert isinstance(H, basestring)
 
132
        if self.get_list('ask_for_intervals_0') == []:
 
133
            return self.ERR_CONFIGURE
 
134
        L, H = utils.adjust_low_high_to_irange(L, H,
 
135
                     self.get_list('ask_for_intervals_0'))
 
136
 
 
137
        if self.m_timeout_handle:
 
138
            gobject.source_remove(self.m_timeout_handle)
 
139
            self.m_timeout_handle = None
 
140
 
 
141
        if solfege.app.m_test_mode:
 
142
            old_tonika = self.m_tonika
 
143
            if old_tonika:
 
144
                old_toptone = old_tonika + self.m_question[0]
 
145
            self.m_P.next_test_question()
 
146
            self.m_question = [self.m_P.m_test_questions[self.m_P.m_test_idx]]
 
147
            #FIXME use tone pitch range from preferences window.
 
148
            self.m_tonika = mpd.MusicalPitch()
 
149
            # Do this loop to make sure two questions in a row does not have
 
150
            # the same top or bottom tone.
 
151
            while True:
 
152
                self.m_tonika.randomize("f", "f'")
 
153
                if not old_tonika:
 
154
                    break
 
155
                if old_tonika != self.m_tonika and self.m_tonika + self.m_question[0] != old_toptone:
 
156
                    break
 
157
            self.q_status = self.QSTATUS_NEW
 
158
            return self.OK
 
159
 
 
160
        if self.get_bool('config/picky_on_new_question') \
 
161
              and self.q_status in [self.QSTATUS_NEW, self.QSTATUS_WRONG]:
 
162
            return self.ERR_PICKY
 
163
 
 
164
        self.q_status = self.QSTATUS_NO
 
165
        last_tonika = self.m_tonika
 
166
        last_question = self.m_question
 
167
        for x in range(10):# we try max 10 times to get a question that
 
168
                           # is different from the last one.
 
169
            self.m_tonika, i = utils.random_tonika_and_interval(L, H,
 
170
                            self.get_list('ask_for_intervals_0'))
 
171
            self.m_question = [i]
 
172
            t = self.m_tonika + i
 
173
            for x in range(1, self.get_int('number_of_intervals=1')):
 
174
                if not self.get_list('ask_for_intervals_%i' % x):
 
175
                    return self.ERR_CONFIGURE
 
176
                i = utils.random_interval(t, L, H,
 
177
                               self.get_list('ask_for_intervals_%i' % x))
 
178
                if not i:
 
179
                    # if we can't find an interval that is with the range
 
180
                    # we, find the interval that is closest to the range
 
181
                    # of notes the user want. This mean that the questions
 
182
                    # are not necessarily that random.
 
183
                    low = mpd.MusicalPitch.new_from_int(L)
 
184
                    high = mpd.MusicalPitch.new_from_int(H)
 
185
                    off = 1000
 
186
                    best = None
 
187
                    for interval in self.get_list('ask_for_intervals_%i'%x):
 
188
                        try:
 
189
                            if t + interval > high:
 
190
                                if t + interval - high < off:
 
191
                                    off = t + interval - high
 
192
                                    best = interval
 
193
                            if t + interval < low:
 
194
                                if low - (t + interval) < off:
 
195
                                    off = low - (t + interval)
 
196
                                    best = interval
 
197
                        except ValueError:
 
198
                            return self.ERR_CONFIGURE
 
199
                    i = best
 
200
                self.m_question.append(i)
 
201
                t = t + i
 
202
            if last_tonika is not None \
 
203
                    and last_tonika == self.m_tonika \
 
204
                    and last_question == self.m_question:
 
205
                continue
 
206
            break
 
207
        self.q_status = self.QSTATUS_NEW
 
208
        return self.OK
 
209
    def play_question(self):
 
210
        if self.q_status == self.QSTATUS_NO:
 
211
            return
 
212
        t = self.m_tonika
 
213
 
 
214
        m = utils.new_track()
 
215
        m.note(4, self.m_tonika.semitone_pitch())
 
216
        for i in self.m_question:
 
217
            t = t + i
 
218
            m.note(4, t.semitone_pitch())
 
219
        soundcard.synth.play_track(m)
 
220
 
 
221
 
 
222
class RhythmAddOnClass:
 
223
    def new_question(self):
 
224
        """returns:
 
225
               self.ERR_PICKY : if the question is not yet solved and the
 
226
                                   teacher is picky (== you have to solve the
 
227
                                   question before a new is asked).
 
228
               self.OK : if a new question was created.
 
229
               self.ERR_NO_ELEMS : if no elements are set to be practised.
 
230
        """
 
231
        if self.m_timeout_handle:
 
232
            gobject.source_remove(self.m_timeout_handle)
 
233
            self.m_timeout_handle = None
 
234
 
 
235
        if self.get_bool('config/picky_on_new_question') \
 
236
                 and self.q_status in [self.QSTATUS_NEW, self.QSTATUS_WRONG]:
 
237
            return self.ERR_PICKY
 
238
 
 
239
        self.q_status = self.QSTATUS_NO
 
240
 
 
241
        norest_v = []
 
242
        v = []
 
243
        for x in self.m_P.header.rhythm_elements:
 
244
            if not (const.RHYTHMS[x][0] == "r"
 
245
                    and self.get_bool("not_start_with_rest")):
 
246
                norest_v.append(x)
 
247
            v.append(x)
 
248
        if not v:
 
249
            return self.ERR_NO_ELEMS
 
250
        if not norest_v:
 
251
            return self.ERR_NO_ELEMS
 
252
        self.m_question = [random.choice(norest_v)]
 
253
        for x in range(1, self.get_int("num_beats")):
 
254
            self.m_question.append(random.choice(v))
 
255
        self.q_status = self.QSTATUS_NEW
 
256
        return self.OK
 
257
    def get_music_notenames(self, count_in):
 
258
        """
 
259
        Return a string with the notenames of the current question.
 
260
        Include count in if count_in == True
 
261
        """
 
262
        s = ""
 
263
        if count_in:
 
264
            if self.m_P.header.count_in_notelen:
 
265
                count_in_notelen = self.m_P.header.count_in_notelen
 
266
            else:
 
267
                count_in_notelen = "4"
 
268
            s = "d%s " % count_in_notelen * self.get_int("count_in")
 
269
        s += " ".join([const.RHYTHMS[k] for k in self.m_question])
 
270
        return s
 
271
    def get_music_string(self):
 
272
        """
 
273
        Return a complete mpd string of the current question that can
 
274
        be feed to utils.play_music.
 
275
        """
 
276
        return r"\staff{%s}" % self.get_music_notenames(True)
 
277
    def play_rhythm(self, rhythm):
 
278
        """
 
279
        rhythm is a string. Example: 'c4 c8 c8 c4'
 
280
        """
 
281
        # FIXME can we use lessonfile.Rhythm insted of this?
 
282
        score = mpd.parser.parse_to_score_object(rhythm)
 
283
        track = score.get_midi_events_as_percussion()[0]
 
284
        track.prepend_bpm(self.get_int("bpm"))
 
285
        track.prepend_volume(cfg.get_int('config/preferred_instrument_volume'))
 
286
        track.replace_note(mpd.notename_to_int("c"),
 
287
                           self.get_int("config/rhythm_perc"))
 
288
        track.replace_note(mpd.notename_to_int("d"),
 
289
                           self.get_int("config/countin_perc"))
 
290
        soundcard.synth.play_track(track)
 
291
    def set_elements_variables(self):
 
292
        """
 
293
        This is called from the on_start_practise() method of exercise
 
294
        modules that generate rhythms and use these variables to select
 
295
        rhythm elements.
 
296
        """
 
297
        if self.m_custom_mode:
 
298
            if not self.m_P.header.rhythm_elements:
 
299
                self.m_P.header.rhythm_elements = self.m_P.header.configurable_rhythm_elements[:3]
 
300
            self.m_P.header.visible_rhythm_elements = self.m_P.header.rhythm_elements[:]
 
301
        else:
 
302
            if not self.m_P.header.visible_rhythm_elements:
 
303
                self.m_P.header.visible_rhythm_elements = \
 
304
                    self.m_P.header.rhythm_elements[:]
 
305
                self.m_P.header.rhythm_elements = \
 
306
                  [n for n in self.m_P.header.rhythm_elements if n != 'newline']
 
307
    def set_default_header_values(self):
 
308
        for n, default in (('bpm', 60),
 
309
                  ('count_in', 2),
 
310
                  ('num_beats', 4)):
 
311
            if n in self.m_P.header:
 
312
                self.set_int(n, self.m_P.header[n])
 
313
            else:
 
314
                self.set_int(n, default)
 
315
 
 
316
class Gui(gtk.VBox, cfg.ConfigUtils, QstatusDefs):
 
317
    """Important members:
 
318
         - practise_box
 
319
         - action_area
 
320
         - config_box
 
321
    
 
322
    """
 
323
    short_delay=700
 
324
    def __init__(self, teacher, no_notebook=False):
 
325
        gtk.VBox.__init__(self)
 
326
        cfg.ConfigUtils.__init__(self, teacher.m_exname)
 
327
        assert type(no_notebook) == bool
 
328
        self._std_buttons = []
 
329
        self.m_key_bindings = {}
 
330
        self.m_t = teacher
 
331
 
 
332
        vbox = gtk.VBox()
 
333
        vbox.set_spacing(gu.PAD)
 
334
        vbox.set_border_width(gu.PAD)
 
335
        vbox.show()
 
336
 
 
337
        self.practise_box = gtk.VBox()
 
338
        self.practise_box.show()
 
339
        vbox.pack_start(self.practise_box, False)
 
340
        self.g_lesson_heading = gtk.Label()
 
341
        self.practise_box.pack_start(self.g_lesson_heading, padding=18)
 
342
 
 
343
        self.action_area = gtk.HBox()
 
344
        self.action_area.show()
 
345
        vbox.pack_start(self.action_area, False)
 
346
 
 
347
        self.config_box = gtk.VBox()
 
348
        self.config_box.set_border_width(gu.PAD)
 
349
        self.config_box.show()
 
350
        self.config_box_sizegroup = gtk.SizeGroup(gtk.SIZE_GROUP_HORIZONTAL)
 
351
        if no_notebook:
 
352
            self.pack_start(vbox)
 
353
            self.pack_start(self.config_box, False)
 
354
            self.g_notebook = None
 
355
        else:
 
356
            self.g_notebook = gtk.Notebook()
 
357
            self.pack_start(self.g_notebook)
 
358
 
 
359
            self.g_notebook.append_page(vbox, gtk.Label(_("Practise")))
 
360
            self.g_notebook.append_page(self.config_box, gtk.Label(_("Config")))
 
361
            self.g_notebook.show()
 
362
        self.g_cancel_test = gtk.Button(_("_Cancel test"))
 
363
        self.g_cancel_test.connect('clicked', self.on_cancel_test)
 
364
        self.action_area.pack_end(self.g_cancel_test, False)
 
365
    def add_module_is_deprecated_label(self):
 
366
        """
 
367
        The deprecated module must set a message in self.g_deprecated_label
 
368
        in on_start_practise, preferable telling the file name of the
 
369
        lesson file.
 
370
        """
 
371
        img = gtk.Image()
 
372
        img.set_from_stock(gtk.STOCK_DIALOG_WARNING,
 
373
                           gtk.ICON_SIZE_BUTTON)
 
374
        hbox = gtk.HBox()
 
375
        hbox.set_border_width(12)
 
376
        self.practise_box.set_child_packing(self.g_lesson_heading, False, False, 0, 0)
 
377
        hbox.set_spacing(6)
 
378
        hbox.pack_start(img)
 
379
        self.g_deprecated_label = gtk.Label()
 
380
        hbox.pack_start(self.g_deprecated_label)
 
381
        self.practise_box.pack_start(hbox, False, False)
 
382
        self.practise_box.reorder_child(hbox, 0)
 
383
    def std_buttons_add(self, *buttons):
 
384
        """
 
385
        buttons is a sequence of tuples ('buttonname', callback)
 
386
        buttonnames with a hyphen '-' are splitted at the first hyphen,
 
387
        and only the first part of the name are used.
 
388
        """
 
389
        d = {'new': _("_New"),
 
390
             'new-interval': _("_New interval"),
 
391
             'new-chord': _("_New chord"),
 
392
             'new-tone': _("_New tone"),
 
393
             'repeat': _("_Repeat"),
 
394
             'repeat_arpeggio': _("Repeat _arpeggio"),
 
395
             'repeat_slowly': _("Repeat _slowly"),
 
396
             'repeat_melodic': _("Repeat _melodic"),
 
397
             'play_tonic': _("Play _tonic"),
 
398
             'play_music': _("P_lay music"),
 
399
             'play_answer': _("_Play answer"),
 
400
             'guess_answer': _("Guess _answer"),
 
401
             'display_music': _("_Display music"),
 
402
             # This button exist mostly for historical reasons, but I think
 
403
             # it can be useful, even though it is not much used today (as in
 
404
             # Solfege 3.6). It will present a Show button for exercises that
 
405
             # does not have a music displayer. And the button will be
 
406
             # insensitive until the question has been solved.
 
407
             'show': _("_Show"),
 
408
             'give_up': _("_Give up")
 
409
        }
 
410
        self._std_buttons.extend([x[0] for x in buttons])
 
411
        for b, cb in buttons:
 
412
            button = gu.bButton(self.action_area, d[b], cb)
 
413
            if '-' in b:
 
414
                b = b.split('-')[0]
 
415
            setattr(self, 'g_%s' % b, button)
 
416
    def _std_buttons_start_sensitivity(self):
 
417
        if 'new' in self._std_buttons:
 
418
            self.g_new.set_sensitive(True)
 
419
        for b in ('repeat', 'repeat_slowly', 'repeat_arpeggio',
 
420
            'repeat_melodic', 'guess_answer', 'play_tonic', 'play_music',
 
421
            'play_answer',
 
422
            'display_music', 'show', 'give_up'):
 
423
            if b in self._std_buttons:
 
424
                getattr(self, 'g_%s' % b).set_sensitive(False)
 
425
    def std_buttons_start_practise(self):
 
426
        self._std_buttons_start_sensitivity()
 
427
        self.g_new.grab_focus()
 
428
        if 'repeat_slowly' in self._std_buttons:
 
429
            if self.m_t.m_P.header.have_repeat_slowly_button:
 
430
                self.g_repeat_slowly.show()
 
431
            else:
 
432
                self.g_repeat_slowly.hide()
 
433
        if 'repeat_arpeggio' in self._std_buttons:
 
434
            # If one or more of the questions is of musictype 'chord', then
 
435
            # we need the "Repeat arpeggio" button.
 
436
            if [q for q in self.m_t.m_P.m_questions \
 
437
                if isinstance(q.music, lessonfile.ChordCommon)]:
 
438
                self.g_repeat_arpeggio.show()
 
439
            else:
 
440
                self.g_repeat_arpeggio.hide()
 
441
        if 'play_tonic' in self._std_buttons:
 
442
            # Display the 'Play tonic' button if any questions in the
 
443
            # lesson file set the 'tonic' variable.
 
444
            if [q for q in self.m_t.m_P.m_questions if 'tonic' in q]:
 
445
                self.g_play_tonic.show()
 
446
            else:
 
447
                self.g_play_tonic.hide()
 
448
        if 'show' in self._std_buttons:
 
449
            # We only want the Show button if there are any questions that can
 
450
            # be displayed.
 
451
            if [q for q in self.m_t.m_P.m_questions if isinstance(q.music, lessonfile.MpdDisplayable)] and not self.m_t.m_P.header.have_music_displayer:
 
452
                self.g_show.show()
 
453
            else:
 
454
                self.g_show.hide()
 
455
    def std_buttons_new_question(self):
 
456
        if 'new' in self._std_buttons:
 
457
            self.g_new.set_sensitive(
 
458
                not self.get_bool('config/picky_on_new_question'))
 
459
        for s in ('repeat', 'repeat_slowly', 'repeat_arpeggio',
 
460
                'repeat_melodic', 'guess_answer', 'play_music',
 
461
                'display_music', 'play_answer'):
 
462
            if s in self._std_buttons:
 
463
                getattr(self, 'g_%s' % s).set_sensitive(True)
 
464
        if 'play_tonic' in self._std_buttons:
 
465
            self.g_play_tonic.set_sensitive(
 
466
                'tonic' in self.m_t.m_P.get_question())
 
467
        if 'show' in self._std_buttons:
 
468
            self.g_show.set_sensitive(False)
 
469
        if 'give_up' in self._std_buttons:
 
470
            self.g_give_up.set_sensitive(False)
 
471
    def std_buttons_end_practise(self):
 
472
        # We check for the lesson parser because it is possible that a
 
473
        # completely broken lesson file was started, and the user tries
 
474
        # to exit that exercise or quit the program. And if the lesson file
 
475
        # does not have a header block, then we cannot call
 
476
        # std_buttons_start_practise
 
477
        if self.m_t.m_P and getattr(self.m_t.m_P, 'header', None):
 
478
            self.std_buttons_start_practise()
 
479
        else:
 
480
            for name in self._std_buttons:
 
481
                if name not in ('new', 'repeat', 'give_up'):
 
482
                    if '-' in name:
 
483
                        name = name.split('-')[0]
 
484
                    getattr(self, 'g_%s' % name).hide()
 
485
    def std_buttons_answer_correct(self):
 
486
        # Setting sensitivity is only required when 
 
487
        # self.get_bool('config/picky_on_new_question') is True, but
 
488
        # we does it every time just in case the user have changed the
 
489
        # variable between clicking 'New' and answering the question.
 
490
        if 'new' in self._std_buttons:
 
491
            self.g_new.set_sensitive(True)
 
492
            self.g_new.grab_focus()
 
493
        if 'show' in self._std_buttons:
 
494
            self.g_show.set_sensitive(True)
 
495
        if 'give_up' in self._std_buttons:
 
496
            self.g_give_up.set_sensitive(False)
 
497
        if 'guess_answer' in self._std_buttons:
 
498
            self.g_guess_answer.set_sensitive(False)
 
499
    def std_buttons_answer_wrong(self):
 
500
        if 'give_up' in self._std_buttons:
 
501
            self.g_give_up.set_sensitive(True)
 
502
    def std_buttons_give_up(self):
 
503
        if 'new' in self._std_buttons:
 
504
            self.g_new.set_sensitive(True)
 
505
            self.g_new.grab_focus()
 
506
        if 'show' in self._std_buttons:
 
507
            self.g_show.set_sensitive(
 
508
                not self.g_music_displayer.props.visible)
 
509
        if 'give_up' in self._std_buttons:
 
510
            self.g_give_up.set_sensitive(False)
 
511
        if 'guess_answer' in self._std_buttons:
 
512
            self.g_guess_answer.set_sensitive(False)
 
513
    def std_buttons_exception_cleanup(self):
 
514
        self._std_buttons_start_sensitivity()
 
515
    def on_cancel_test(self, *w):
 
516
        self.g_cancel_test.hide()
 
517
        self.on_end_practise()
 
518
        solfege.win.exit_test_mode()
 
519
    def do_test_complete(self):
 
520
        self.on_end_practise()
 
521
        req = self.m_t.m_P.get_test_requirement()
 
522
        self.g_cancel_test.hide()
 
523
        solfege.win.exit_test_mode()
 
524
        passed, res = solfege.db.get_test_status(self.m_t.m_P.m_filename)
 
525
        if res >= req:
 
526
            gu.dialog_ok(_("Test completed!\nYour score was %(score).1f%%.\nThe test requirement was %(requirement).1f%%.") % {'score': res * 100, 'requirement': req * 100})
 
527
        else:
 
528
            gu.dialog_ok(_("Test failed.\nYour score was %(score).1f%%.\nThe test requirement was %(requirement).1f%%.") % {'score': res * 100, 'requirement': req * 100})
 
529
    def set_lesson_heading(self, txt):
 
530
        if txt:
 
531
            self.g_lesson_heading.set_text('<span size="large"><b>%s</b></span>' % gu.escape(txt))
 
532
            self.g_lesson_heading.set_use_markup(True)
 
533
            self.g_lesson_heading.show()
 
534
        else:
 
535
            self.g_lesson_heading.hide()
 
536
    def on_start_practise(self):
 
537
        """
 
538
        Code that are common for all exercises. Not used by many now,
 
539
        but lets see if that can improve.
 
540
        """
 
541
        self.handle_config_box_visibility()
 
542
        self.handle_statistics_page_sensibility()
 
543
    def on_end_practise(self):
 
544
        pass
 
545
    def handle_config_box_visibility(self):
 
546
        """
 
547
        Show self.config_box if it has any visible children, otherwise
 
548
        hide it.
 
549
        """
 
550
        if [c for c in self.config_box.get_children() \
 
551
            if c.get_property('visible')]:
 
552
            self.config_box.show()
 
553
        else:
 
554
            self.config_box.hide()
 
555
    def handle_statistics_page_sensibility(self):
 
556
        try:
 
557
            if self.m_t.m_custom_mode:
 
558
                self.g_statview.set_sensitive(False)
 
559
                self.g_statview.set_tooltip_text(_("Statistics is disabled. Either because you selected a “Configure yourself” exercise, or because “Expert mode” is selected in the preferences window."))
 
560
            else:
 
561
                self.g_statview.set_sensitive(True)
 
562
                self.g_statview.set_tooltip_text(None)
 
563
        except AttributeError: # not all exercises has g_statview
 
564
            pass
 
565
    def on_key_press_event(self, widget, event):
 
566
        if (self.g_notebook is None or self.g_notebook.get_current_page() == 0) \
 
567
           and event.type == gtk.gdk.KEY_PRESS:
 
568
            for s in self.m_key_bindings:
 
569
                if self.keymatch(event, s):
 
570
                    self.m_key_bindings[s]()
 
571
                    return 1
 
572
    def keymatch(self, event, cfname):
 
573
        a, b = gu.parse_key_string(self.get_string(cfname))
 
574
        return ((event.state & (gtk.gdk.CONTROL_MASK|gtk.gdk.SHIFT_MASK|gtk.gdk.MOD1_MASK)) == a) and (event.keyval == b)
 
575
    def setup_statisticsviewer(self, viewclass, heading):
 
576
        self.g_statview = viewclass(self.m_t.m_statistics, heading)
 
577
        self.g_statview.show()
 
578
        self.g_notebook.append_page(self.g_statview, gtk.Label(_("Statistics")))
 
579
        self.g_notebook.connect('switch_page', self.on_switch_page)
 
580
    def on_switch_page(self, notebook, obj, pagenum):
 
581
        if pagenum == 2:
 
582
            if self.m_t.m_P and not self.m_t.m_custom_mode:
 
583
                self.g_statview.update()
 
584
            else:
 
585
                self.g_statview.clear()
 
586
    def _add_auto_new_question_gui(self, box):
 
587
        hbox = gu.bHBox(box, False)
 
588
        hbox.set_spacing(gu.PAD_SMALL)
 
589
        adj = gtk.Adjustment(0, 0, 10, 0.1, 1)
 
590
        spin = gu.nSpinButton(self.m_exname, 'seconds_before_new_question',
 
591
                       adj)
 
592
        spin.set_digits(1)
 
593
        label = gtk.Label(_("Delay (seconds):"))
 
594
        label.show()
 
595
        def f(button, spin=spin, label=label):
 
596
            spin.set_sensitive(button.get_active())
 
597
            label.set_sensitive(button.get_active())
 
598
        b = gu.nCheckButton(self.m_exname, 'new_question_automatically',
 
599
                            _("_New question automatically."), callback=f)
 
600
        hbox.pack_start(b, False)
 
601
        label.set_sensitive(b.get_active())
 
602
        hbox.pack_start(label, False)
 
603
        spin.set_sensitive(b.get_active())
 
604
        hbox.pack_start(spin, False)
 
605
    def _lessonfile_exception(self, exception, sourcefile, lineno):
 
606
        m = ExceptionDialog(exception)
 
607
        idx = self.m_t.m_P._idx
 
608
        if idx is not None:
 
609
            m.add_text(_('Please check question number %(idx)i in the lesson file "%(lf)s".') % {'idx': self.m_t.m_P._idx+1, 'lf': self.m_t.m_P.m_filename})
 
610
        if 'm_nonwrapped_text' in dir(exception):
 
611
            m.add_nonwrapped_text(exception.m_nonwrapped_text)
 
612
        if 'm_mpd_badcode' in dir(exception) and exception.m_mpd_badcode:
 
613
            # some music classes may return an empty string if they have
 
614
            # nothing useful to display.
 
615
            m.add_nonwrapped_text(exception.m_mpd_badcode)
 
616
        m.add_text(_('The exception was caught in\n"%(filename)s", line %(lineno)i.') % {'filename': sourcefile, 'lineno': lineno})
 
617
        m.run()
 
618
        m.destroy()
 
619
    def _mpd_exception(self, exception, sourcefile, lineno):
 
620
        m = ExceptionDialog(exception)
 
621
        if 'm_mpd_varname' in dir(exception):
 
622
            m.add_text(_('Failed to parse the music in the variable "%(varname)s" in question number %(idx)i in the lesson file "%(lf)s".') % {
 
623
                'idx': self.m_t.m_P._idx + 1,
 
624
                'lf': lessonfile.uri_expand(self.m_t.m_P.m_filename),
 
625
                'varname': exception.m_mpd_varname})
 
626
        else:
 
627
            m.add_text(_('Failed to parse the music for question number %(idx)i in the lesson file "%(lf)s".') % {'idx': self.m_t.m_P._idx + 1, 'lf': lessonfile.uri_expand(self.m_t.m_P.m_filename)})
 
628
        if 'm_mpd_badcode' in dir(exception):
 
629
            m.add_nonwrapped_text(exception.m_mpd_badcode)
 
630
        m.add_text(_('The exception was caught in\n"%(filename)s", line %(lineno)i.') % {'filename': sourcefile, 'lineno': lineno})
 
631
        m.run()
 
632
        m.destroy()
 
633
    def run_exception_handled(self, method, *args, **kwargs):
 
634
        """
 
635
        Call method() and catch exceptions with standard_exception_handler.
 
636
        """
 
637
        try:
 
638
            return method(*args, **kwargs)
 
639
        except Exception, e:
 
640
            if not self.standard_exception_handler(e):
 
641
                raise
 
642
    def standard_exception_handler(self, e, cleanup_function=lambda: False):
 
643
        """
 
644
        Use this method to try to catch a few common solfege exceptions.
 
645
        It should only be used to catch exceptions after the file has
 
646
        successfully parsed by the lessonfile parser, and only used in
 
647
        exercises where the mpd code that might be wrong is comming from
 
648
        lesson files, not generated code.
 
649
 
 
650
        Usage:
 
651
        try:
 
652
            do something
 
653
        except Exception, e:
 
654
            if not self.standard_exception_handler(e):
 
655
                raise
 
656
        """
 
657
        sourcefile, lineno, func, code = traceback.extract_tb(sys.exc_info()[2])[0]
 
658
        # We can replace characters because we will only display the
 
659
        # file name, not open the file.
 
660
        sourcefile = sourcefile.decode(sys.getfilesystemencoding(), 'replace')
 
661
        if solfege.app.m_options.disable_exception_handler:
 
662
            return False
 
663
        elif isinstance(e, lessonfile.NoQuestionsConfiguredException):
 
664
            cleanup_function()
 
665
            solfege.win.display_error_message2(e.args[0], e.args[1])
 
666
            return True
 
667
        elif isinstance(e, lessonfile.LessonfileException):
 
668
            cleanup_function()
 
669
            self._lessonfile_exception(e, sourcefile, lineno)
 
670
            return True
 
671
        elif isinstance(e, mpd.MpdException):
 
672
            cleanup_function()
 
673
            self._mpd_exception(e, sourcefile, lineno)
 
674
            return True
 
675
        elif isinstance(e, osutils.BinaryBaseException):
 
676
            cleanup_function()
 
677
            solfege.win.display_error_message2(e.msg1, e.msg2)
 
678
            return True
 
679
        elif isinstance(e, osutils.OsUtilsException):
 
680
            cleanup_function()
 
681
            solfege.win.display_exception_message(e)
 
682
            return True
 
683
        return False
 
684
 
 
685
class RhythmAddOnGuiClass(object):
 
686
    def add_select_elements_gui(self):
 
687
        self.g_element_frame = frame = gtk.Frame(_("Rhythms to use in question"))
 
688
        self.config_box.pack_start(frame, False)
 
689
        self.g_select_rhythms_box = gu.NewLineBox()
 
690
        self.g_select_rhythms_box.set_border_width(gu.PAD_SMALL)
 
691
        frame.add(self.g_select_rhythms_box)
 
692
    def add_select_num_beats_gui(self):
 
693
        ###
 
694
        hbox = gtk.HBox()
 
695
        hbox.set_spacing(gu.hig.SPACE_SMALL)
 
696
        label = gtk.Label(_("Number of beats in question:"))
 
697
        hbox.pack_start(label, False)
 
698
        self.config_box_sizegroup.add_widget(label)
 
699
        label.set_alignment(1.0, 0.5)
 
700
        hbox.pack_start(gu.nSpinButton(self.m_exname, "num_beats",
 
701
                     gtk.Adjustment(4, 1, 100, 1, 10)), False)
 
702
        self.config_box.pack_start(hbox, False)
 
703
        hbox.show_all()
 
704
        #
 
705
        hbox = gtk.HBox()
 
706
        hbox.set_spacing(gu.hig.SPACE_SMALL)
 
707
        label = gtk.Label(_("Count in before question:"))
 
708
        hbox.pack_start(label, False)
 
709
        self.config_box_sizegroup.add_widget(label)
 
710
        label.set_alignment(1.0, 0.5)
 
711
        hbox.pack_start(label, False)
 
712
        hbox.pack_start(gu.nSpinButton(self.m_exname, "count_in",
 
713
                     gtk.Adjustment(2, 0, 10, 1, 10)), False)
 
714
        hbox.show_all()
 
715
        self.config_box.pack_start(hbox, False)
 
716
    def pngcheckbutton(self, i):
 
717
        btn = gtk.CheckButton()
 
718
        btn.add(gu.create_rhythm_image(const.RHYTHMS[i]))
 
719
        btn.show()
 
720
        btn.connect('clicked', self.select_element_cb, i)
 
721
        return btn
 
722
    def update_select_elements_buttons(self):
 
723
        """
 
724
        (Re)create the checkbuttons used to select which rhythm elements
 
725
        to be used when creating questions. We only need to do this if
 
726
        we are in m_custom_mode.
 
727
        """
 
728
        self.g_select_rhythms_box.empty()
 
729
        for n in self.m_t.m_P.header.configurable_rhythm_elements:
 
730
            if n == 'newline':
 
731
                self.g_select_rhythms_box.newline()
 
732
            else:
 
733
                b = self.pngcheckbutton(n)
 
734
                self.g_select_rhythms_box.add_widget(b)
 
735
                b.set_active(n in self.m_t.m_P.header.rhythm_elements)
 
736
        self.g_select_rhythms_box.show_widgets()
 
737
    def select_element_cb(self, button, element_num):
 
738
        def sortlike(orig, b):
 
739
            ret = []
 
740
            for n in orig:
 
741
                if n == 'newline':
 
742
                    ret.append('newline')
 
743
                elif n in b:
 
744
                    ret.append(n)
 
745
            return ret
 
746
        if button.get_active():
 
747
            if element_num not in self.m_t.m_P.header.rhythm_elements:
 
748
                self.m_t.m_P.header.rhythm_elements.append(element_num)
 
749
                self.m_t.m_P.header.rhythm_elements = sortlike(
 
750
                    self.m_t.m_P.header.configurable_rhythm_elements,
 
751
                    self.m_t.m_P.header.rhythm_elements)
 
752
        else:
 
753
            if element_num in self.m_t.m_P.header.rhythm_elements:
 
754
                self.m_t.m_P.header.rhythm_elements.remove(element_num)
 
755
        self.m_t.m_P.header.visible_rhythm_elements = \
 
756
            self.m_t.m_P.header.rhythm_elements[:]
 
757
        self.m_t.m_P.header.rhythm_elements = \
 
758
            [n for n in self.m_t.m_P.header.rhythm_elements if n != 'newline']
 
759
 
 
760
class IntervalGui(Gui):
 
761
    """
 
762
    Creates 'New interval' and 'Repeat' buttons in the action_area.
 
763
    """
 
764
    keyboard_accel = 99
 
765
    def __init__(self, teacher):
 
766
        Gui.__init__(self, teacher)
 
767
 
 
768
        self.g_input = None
 
769
 
 
770
        self.g_flashbar = gu.FlashBar()
 
771
        self.g_flashbar.show()
 
772
        self.practise_box.pack_start(self.g_flashbar, False)
 
773
        self.practise_box.set_spacing(gu.PAD)
 
774
 
 
775
        self.std_buttons_add(('new-interval', self.new_question),
 
776
            ('repeat', self.repeat_question))
 
777
        self.setup_key_bindings()
 
778
    def _create_select_inputwidget_gui(self):
 
779
        """
 
780
        This will be called by HarmonicInterval and MelodicInterval
 
781
        constructor
 
782
        """
 
783
        hbox = gu.bHBox(self.config_box, False)
 
784
        hbox.set_spacing(gu.PAD_SMALL)
 
785
        gu.bLabel(hbox, _("Input interface:"), False)
 
786
 
 
787
        combo = gtk.combo_box_new_text()
 
788
        for i in range(len(inputwidgets.inputwidget_names)):
 
789
            combo.append_text(inputwidgets.inputwidget_names[i])
 
790
        if self.get_int('inputwidget') < len(inputwidgets.inputwidget_names):
 
791
            combo.set_active(self.get_int('inputwidget'))
 
792
        else:
 
793
            combo.set_active(0)
 
794
        combo.connect('changed', lambda w: self.use_inputwidget(w.get_active()))
 
795
        hbox.pack_start(combo, False)
 
796
 
 
797
        self.g_disable_unused_buttons = gu.nCheckButton(self.m_exname,
 
798
                    'disable_unused_intervals', _("_Disable unused buttons"))
 
799
        hbox.pack_start(self.g_disable_unused_buttons)
 
800
    def select_inputwidget(self):
 
801
        """
 
802
        This will be called by HarmonicInterval and MelodicInterval
 
803
        constructor
 
804
        """
 
805
        i = self.get_int('inputwidget')
 
806
        if i >= len(inputwidgets.inputwidget_names):
 
807
            i = 0
 
808
        self.use_inputwidget(i)
 
809
    def use_inputwidget(self, i):
 
810
        self.set_int('inputwidget', i)
 
811
        if self.g_input:
 
812
            self.g_input.destroy()
 
813
        # FIXME UGH ugly ugly ugly, I'm lazy lazy lazy
 
814
        import solfege.exercises.harmonicinterval
 
815
        if isinstance(self, solfege.exercises.harmonicinterval.Gui):
 
816
            v = ['intervals']
 
817
        else:
 
818
            v = []
 
819
            for x in range(self.get_int('maximum_number_of_intervals')):
 
820
                v.append('ask_for_intervals_%i' % x)
 
821
        if i == 0:
 
822
            assert inputwidgets.inputwidget_names[i] == _("Buttons")
 
823
            self.g_input = inputwidgets.IntervalButtonsWidget(self.m_exname,
 
824
                  'intervals', self.click_on_interval, self.get_interval_input_list, v)
 
825
        else:
 
826
            self.g_input = inputwidgets.name_to_inputwidget(
 
827
                                 inputwidgets.inputwidget_names[i],
 
828
                                 self.click_on_interval)
 
829
        self.practise_box.pack_start(self.g_input)
 
830
        self.practise_box.reorder_child(self.g_input, 1)
 
831
        self.g_input.show()
 
832
        if self.m_t.m_tonika:
 
833
            # Don't call on_end_practise if we are starting up the exercise.
 
834
            # This whole thing is a mess.
 
835
            self.on_end_practise()
 
836
        self.g_disable_unused_buttons.set_sensitive(self.get_int('inputwidget')==0)
 
837
    def setup_key_bindings(self):
 
838
        keys = ['minor2', 'major2', 'minor3', 'major3',
 
839
                'perfect4', 'diminished5', 'perfect5', 'minor6',
 
840
                'major6', 'minor7', 'major7', 'perfect8',
 
841
                'minor9', 'major9', 'minor10', 'major10']
 
842
        self.m_key_bindings = {}
 
843
        for idx in range(len(keys)):
 
844
            self.m_key_bindings['interval_input/'+keys[idx]] = lambda idx=idx, self=self: self.click_on_interval(self.keyboard_accel, idx+1, None)
 
845
    def repeat_question(self, *w):
 
846
        self.m_t.play_question()
 
847
        self.g_input.grab_focus_first_sensitive_button()
 
848
 
 
849
 
 
850
class LessonbasedGui(Gui):
 
851
    def __init__(self, teacher, no_notebook=False):
 
852
        Gui.__init__(self, teacher, no_notebook)
 
853
    def add_random_transpose_gui(self):
 
854
        self.g_random_transpose_box = hbox = gu.bHBox(self.config_box, False, False)
 
855
        label = gtk.Label(_("Random transpose:"))
 
856
        label.show()
 
857
        hbox.pack_start(label, False)
 
858
        hbox.set_spacing(6)
 
859
        self.g_random_transpose = gtk.Label()
 
860
        self.g_random_transpose.show()
 
861
        hbox.pack_start(self.g_random_transpose)
 
862
 
 
863
        button = gtk.Button(_("Change ..."))
 
864
        button.show()
 
865
        button.connect('clicked', self.run_random_transpose_dialog)
 
866
        hbox.pack_start(button)
 
867
    def run_random_transpose_dialog(self, widget):
 
868
        dlg = RandomTransposeDialog(self.m_t.m_P.header.random_transpose, solfege.win)
 
869
        response = dlg.run()
 
870
        if response == gtk.RESPONSE_OK:
 
871
            self.m_t.m_P.header.random_transpose = dlg.get_value()
 
872
            if self.m_t.m_P.header.random_transpose[0] == True:
 
873
                self.g_random_transpose.set_text(_("Yes"))
 
874
            elif self.m_t.m_P.header.random_transpose[0] == False:
 
875
                self.g_random_transpose.set_text(_("No"))
 
876
            else:
 
877
                self.g_random_transpose.set_text(str(self.m_t.m_P.header.random_transpose))
 
878
        dlg.destroy()
 
879
    def show_answer(self, widget=None):
 
880
        """
 
881
        Show the answer in the g_music_displayer if we have one, if not
 
882
        use a new window.
 
883
        """
 
884
        if 'vmusic' in self.m_t.m_P.get_question():
 
885
            varname = 'vmusic'
 
886
        else:
 
887
            varname = 'music'
 
888
        if not isinstance(self.m_t.m_P.get_question()[varname], lessonfile.MpdDisplayable):
 
889
            return
 
890
        self.display_music(varname)
 
891
    def display_music(self, varname):
 
892
        """
 
893
        Display the music in the variable named by varname from
 
894
        the currently selected question. This method will handle
 
895
        the normal mpd and lessonfile exceptions.
 
896
        """
 
897
        try:
 
898
            if self.m_t.m_P.header.have_music_displayer:
 
899
                fontsize = self.get_int('config/feta_font_size=20')
 
900
                self.g_music_displayer.display(self.m_t.m_P.get_music(varname), fontsize)
 
901
            else:
 
902
                solfege.win.display_in_musicviewer(self.m_t.m_P.get_music(varname))
 
903
        except mpd.MpdException, e:
 
904
            self.m_t.m_P.get_question()[varname].complete_to_musicdata_coords(self.m_t.m_P, e)
 
905
            e.m_mpd_varname = varname
 
906
            e.m_mpd_badcode = self.m_t.m_P.get_question()[varname].get_err_context(e, self.m_t.m_P)
 
907
            if not self.standard_exception_handler(e):
 
908
                raise
 
909
    def do_at_question_start_show_play(self):
 
910
        """
 
911
        This method is shared by idbyname and elembuilder, and possibly
 
912
        other exercises later. It will show and/or play music based on
 
913
        the header.at_question_start  variable.
 
914
 
 
915
        It might raise mpd.MpdException.
 
916
        """
 
917
        if self.m_t.m_P.header.at_question_start == 'show':
 
918
            self.show_answer()
 
919
        elif self.m_t.m_P.header.at_question_start == 'play':
 
920
            self.m_t.m_P.play_question()
 
921
            if 'cuemusic' in self.m_t.m_P.get_question():
 
922
                self.display_music('cuemusic')
 
923
        else:
 
924
            self.m_t.m_P.play_question()
 
925
            if 'show' in self.m_t.m_P.header.at_question_start \
 
926
                and 'play' in self.m_t.m_P.header.at_question_start:
 
927
                self.show_answer()
 
928
            elif 'cuemusic' in self.m_t.m_P.get_question():
 
929
                self.display_music('cuemusic')
 
930
    def show_hide_at_question_start_buttons(self):
 
931
        """
 
932
        Show and hide g_play_music, g_repeat and g_display_music
 
933
        depending on the content of header.at_question_start.
 
934
        This method is used by at least idbyname and elembuilder.
 
935
        """
 
936
        if self.m_t.m_P.header.at_question_start == 'show':
 
937
            self.g_play_music.show()
 
938
            self.g_repeat.hide()
 
939
        else:
 
940
            self.g_play_music.hide()
 
941
            self.g_repeat.show()
 
942
        if self.m_t.m_P.header.at_question_start == 'play':
 
943
            self.g_display_music.show()
 
944
        else:
 
945
            self.g_display_music.hide()
 
946