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
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.
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.
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/>.
18
from __future__ import absolute_import
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
42
QSTATUS_VOICING_SOLVED = 5
43
QSTATUS_VOICING_WRONG = 6
44
QSTATUS_TYPE_WRONG = 7
45
QSTATUS_TYPE_SOLVED = 8
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
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):
72
Shared between harmonic and melodic interval.
74
self.m_statistics.exit_test_mode()
75
def set_lessonfile(self, lessonfile):
77
Set the variable 'm_lessonfile' and
78
parse the lesson file and save the statistics.
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:
90
self.m_P = self.lessonfileclass()
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.
97
solfege.win.display_error_message(
98
"""There was an IOError while trying to parse a lesson file:
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?"))
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):
110
Play the tonic of the question, if defined.
112
if 'tonic' in self.m_P.get_question():
113
self.m_P.play_question(None, 'tonic')
115
class MelodicIntervalTeacher(Teacher):
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
125
def __init__(self, exname):
126
Teacher.__init__(self, exname)
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'))
137
if self.m_timeout_handle:
138
gobject.source_remove(self.m_timeout_handle)
139
self.m_timeout_handle = None
141
if solfege.app.m_test_mode:
142
old_tonika = self.m_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.
152
self.m_tonika.randomize("f", "f'")
155
if old_tonika != self.m_tonika and self.m_tonika + self.m_question[0] != old_toptone:
157
self.q_status = self.QSTATUS_NEW
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
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))
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)
187
for interval in self.get_list('ask_for_intervals_%i'%x):
189
if t + interval > high:
190
if t + interval - high < off:
191
off = t + interval - high
193
if t + interval < low:
194
if low - (t + interval) < off:
195
off = low - (t + interval)
198
return self.ERR_CONFIGURE
200
self.m_question.append(i)
202
if last_tonika is not None \
203
and last_tonika == self.m_tonika \
204
and last_question == self.m_question:
207
self.q_status = self.QSTATUS_NEW
209
def play_question(self):
210
if self.q_status == self.QSTATUS_NO:
214
m = utils.new_track()
215
m.note(4, self.m_tonika.semitone_pitch())
216
for i in self.m_question:
218
m.note(4, t.semitone_pitch())
219
soundcard.synth.play_track(m)
222
class RhythmAddOnClass:
223
def new_question(self):
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.
231
if self.m_timeout_handle:
232
gobject.source_remove(self.m_timeout_handle)
233
self.m_timeout_handle = None
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
239
self.q_status = self.QSTATUS_NO
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")):
249
return self.ERR_NO_ELEMS
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
257
def get_music_notenames(self, count_in):
259
Return a string with the notenames of the current question.
260
Include count in if count_in == True
264
if self.m_P.header.count_in_notelen:
265
count_in_notelen = self.m_P.header.count_in_notelen
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])
271
def get_music_string(self):
273
Return a complete mpd string of the current question that can
274
be feed to utils.play_music.
276
return r"\staff{%s}" % self.get_music_notenames(True)
277
def play_rhythm(self, rhythm):
279
rhythm is a string. Example: 'c4 c8 c8 c4'
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):
293
This is called from the on_start_practise() method of exercise
294
modules that generate rhythms and use these variables to select
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[:]
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),
311
if n in self.m_P.header:
312
self.set_int(n, self.m_P.header[n])
314
self.set_int(n, default)
316
class Gui(gtk.VBox, cfg.ConfigUtils, QstatusDefs):
317
"""Important members:
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 = {}
333
vbox.set_spacing(gu.PAD)
334
vbox.set_border_width(gu.PAD)
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)
343
self.action_area = gtk.HBox()
344
self.action_area.show()
345
vbox.pack_start(self.action_area, False)
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)
352
self.pack_start(vbox)
353
self.pack_start(self.config_box, False)
354
self.g_notebook = None
356
self.g_notebook = gtk.Notebook()
357
self.pack_start(self.g_notebook)
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):
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
372
img.set_from_stock(gtk.STOCK_DIALOG_WARNING,
373
gtk.ICON_SIZE_BUTTON)
375
hbox.set_border_width(12)
376
self.practise_box.set_child_packing(self.g_lesson_heading, False, False, 0, 0)
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):
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.
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.
408
'give_up': _("_Give up")
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)
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',
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()
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()
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()
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
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:
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()
480
for name in self._std_buttons:
481
if name not in ('new', 'repeat', 'give_up'):
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)
526
gu.dialog_ok(_("Test completed!\nYour score was %(score).1f%%.\nThe test requirement was %(requirement).1f%%.") % {'score': res * 100, 'requirement': req * 100})
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):
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()
535
self.g_lesson_heading.hide()
536
def on_start_practise(self):
538
Code that are common for all exercises. Not used by many now,
539
but lets see if that can improve.
541
self.handle_config_box_visibility()
542
self.handle_statistics_page_sensibility()
543
def on_end_practise(self):
545
def handle_config_box_visibility(self):
547
Show self.config_box if it has any visible children, otherwise
550
if [c for c in self.config_box.get_children() \
551
if c.get_property('visible')]:
552
self.config_box.show()
554
self.config_box.hide()
555
def handle_statistics_page_sensibility(self):
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."))
561
self.g_statview.set_sensitive(True)
562
self.g_statview.set_tooltip_text(None)
563
except AttributeError: # not all exercises has g_statview
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]()
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):
582
if self.m_t.m_P and not self.m_t.m_custom_mode:
583
self.g_statview.update()
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',
593
label = gtk.Label(_("Delay (seconds):"))
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
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})
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})
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})
633
def run_exception_handled(self, method, *args, **kwargs):
635
Call method() and catch exceptions with standard_exception_handler.
638
return method(*args, **kwargs)
640
if not self.standard_exception_handler(e):
642
def standard_exception_handler(self, e, cleanup_function=lambda: False):
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.
654
if not self.standard_exception_handler(e):
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:
663
elif isinstance(e, lessonfile.NoQuestionsConfiguredException):
665
solfege.win.display_error_message2(e.args[0], e.args[1])
667
elif isinstance(e, lessonfile.LessonfileException):
669
self._lessonfile_exception(e, sourcefile, lineno)
671
elif isinstance(e, mpd.MpdException):
673
self._mpd_exception(e, sourcefile, lineno)
675
elif isinstance(e, osutils.BinaryBaseException):
677
solfege.win.display_error_message2(e.msg1, e.msg2)
679
elif isinstance(e, osutils.OsUtilsException):
681
solfege.win.display_exception_message(e)
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):
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)
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)
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]))
720
btn.connect('clicked', self.select_element_cb, i)
722
def update_select_elements_buttons(self):
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.
728
self.g_select_rhythms_box.empty()
729
for n in self.m_t.m_P.header.configurable_rhythm_elements:
731
self.g_select_rhythms_box.newline()
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):
742
ret.append('newline')
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)
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']
760
class IntervalGui(Gui):
762
Creates 'New interval' and 'Repeat' buttons in the action_area.
765
def __init__(self, teacher):
766
Gui.__init__(self, teacher)
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)
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):
780
This will be called by HarmonicInterval and MelodicInterval
783
hbox = gu.bHBox(self.config_box, False)
784
hbox.set_spacing(gu.PAD_SMALL)
785
gu.bLabel(hbox, _("Input interface:"), False)
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'))
794
combo.connect('changed', lambda w: self.use_inputwidget(w.get_active()))
795
hbox.pack_start(combo, False)
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):
802
This will be called by HarmonicInterval and MelodicInterval
805
i = self.get_int('inputwidget')
806
if i >= len(inputwidgets.inputwidget_names):
808
self.use_inputwidget(i)
809
def use_inputwidget(self, i):
810
self.set_int('inputwidget', i)
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):
819
for x in range(self.get_int('maximum_number_of_intervals')):
820
v.append('ask_for_intervals_%i' % x)
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)
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)
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()
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:"))
857
hbox.pack_start(label, False)
859
self.g_random_transpose = gtk.Label()
860
self.g_random_transpose.show()
861
hbox.pack_start(self.g_random_transpose)
863
button = gtk.Button(_("Change ..."))
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)
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"))
877
self.g_random_transpose.set_text(str(self.m_t.m_P.header.random_transpose))
879
def show_answer(self, widget=None):
881
Show the answer in the g_music_displayer if we have one, if not
884
if 'vmusic' in self.m_t.m_P.get_question():
888
if not isinstance(self.m_t.m_P.get_question()[varname], lessonfile.MpdDisplayable):
890
self.display_music(varname)
891
def display_music(self, varname):
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.
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)
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):
909
def do_at_question_start_show_play(self):
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.
915
It might raise mpd.MpdException.
917
if self.m_t.m_P.header.at_question_start == 'show':
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')
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:
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):
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.
936
if self.m_t.m_P.header.at_question_start == 'show':
937
self.g_play_music.show()
940
self.g_play_music.hide()
942
if self.m_t.m_P.header.at_question_start == 'play':
943
self.g_display_music.show()
945
self.g_display_music.hide()