# -*- coding: iso-8859-1 -*- # GNU Solfege - free ear training software # Copyright (C) 2004, 2005, 2006, 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 from __future__ import division import pygtk pygtk.require("2.0") import gtk import sys import mpd import mpd.musicdisplayer import gu import os import lessonfile import dataparser import htmlwidget import stock gtk.stock_add([('solfege-notehead', _("Add noteheads"), 0, 0, ''), ('solfege-sharp', _("Add sharp"), 0, 0, ''), ('solfege-double-sharp', _("Add double-sharp"), 0, 0, ''), ('solfege-natural', _("Remove accidentals"), 0, 0, ''), ('solfege-flat', _("Add flat"), 0, 0, ''), ('solfege-double-flat', _("Add double-flat"), 0, 0, ''), ('solfege-erase', _("Delete tone"), 0, 0, ''), ]) app_version = "0.1.4" class HelpWindow(gtk.Window): def __init__(self, parent): gtk.Window.__init__(self) self.set_title(_("GNU Solfege lesson file editor") ) self.set_default_size(400, 400) self.g_parent = parent self.vbox = gtk.VBox() self.vbox.set_spacing(8) self.add(self.vbox) self.connect('delete_event', self.delete_cb) self.g_htmlwidget = htmlwidget.HtmlWidget(None, None) self.vbox.pack_start(self.g_htmlwidget) self.vbox.pack_start(gtk.HSeparator(), False) bbox = gtk.HButtonBox() bbox.set_border_width(8) self.vbox.pack_start(bbox, False) b = gtk.Button(stock=gtk.STOCK_CLOSE) b.connect('clicked', self.close_cb) bbox.pack_start(b) self.show_all() self.set_focus(b) def source(self, html): self.g_htmlwidget.source(html) def delete_cb(self, *v): self.g_parent.g_help_window = None def close_cb(self, w): self.g_parent.g_help_window = None self.destroy() window_actions = [ ('FileMenu', None, _('_File')), ('NewLessonfile', gtk.STOCK_NEW, None, None, 'new file', 'file_new_cb'), ('Open', gtk.STOCK_OPEN, None, None, 'Open lesson file', 'file_open_cb'), ('Save', gtk.STOCK_SAVE, None, None, 'Save the lesson file', 'file_save_cb'), ('SaveAs', gtk.STOCK_SAVE_AS, None, 's', 'Save the lesson file with a new name', 'file_save_as_cb'), ('Quit', gtk.STOCK_QUIT, None, None, 'Quit program', 'quit_cb'), ('HelpMenu', None, _('_Help')), ('HelpHelp', gtk.STOCK_HELP, None, None, None, 'help_cb'), ('HelpAbout', None, _('_About'), '', '', 'about_cb'), ] lessonfile_actions = [ ('GotoFirstQuestion', gtk.STOCK_GOTO_FIRST, None, None, _('Go to the first question'), 'goto_first_question_cb'), ('GoBackQuestion', gtk.STOCK_GO_BACK, None, None, _('Go to the previous question'), 'go_back_question_cb'), ('GoForwardQuestion', gtk.STOCK_GO_FORWARD, None, None, _('Go to the next question'), 'go_forward_question_cb'), ('GotoLastQuestion', gtk.STOCK_GOTO_LAST, None, None, _('Go to the last question'), 'goto_last_question_cb'), ('NewQuestion', gtk.STOCK_ADD, None, None, _('Add a new question'), 'new_question_cb'), ('NoteheadCursor', 'solfege-notehead', None, None, _('Add noteheads'), 'select_cursor_notehead_cb'), ('SharpCursor', 'solfege-sharp', None, None, _('Add sharps'), 'select_cursor_sharp_cb'), ('DoubleSharpCursor', 'solfege-double-sharp', None, None, _('Add double-sharps'), 'select_cursor_2sharp_cb'), ('NaturalCursor', 'solfege-natural', None, None, _('Remove accidentals'), 'select_cursor_natural_cb'), ('FlatCursor', 'solfege-flat', None, None, _('Add flats'), 'select_cursor_flat_cb'), ('DoubleFlatCursor', 'solfege-double-flat', None, None, _('Add double-flats'), 'select_cursor_2flat_cb'), ('EraseCursor', 'solfege-erase', None, None, _('Erase tones'), 'select_cursor_erase_cb'), ] ui_string = """ """ def fix_actions(actions, instance): "Helper function to map methods to an instance" retval = [] for i in range(len(actions)): curr = actions[i] if len(curr) > 5: curr = list(curr) curr[5] = getattr(instance, curr[5]) curr = tuple(curr) retval.append(curr) return retval class EditorLessonfile(object): def __init__(self): self.m_filename = None self.m_changed = False self.header = lessonfile._Header({'module': 'chord'}) self.m_questions = [dataparser.Question()] self.m_questions[-1]['music'] = lessonfile.Music("", "chord") self.m_questions[-1]['name'] = "" self._idx = 0 class MainWin(gtk.Window): def __init__(self, datadir): gtk.Window.__init__(self) self.icons = stock.EditorIconFactory(self, datadir) self.connect('destroy', lambda w: gtk.main_quit()) self.g_help_window = None # toplevel_vbox: # -menubar # -toolbar # -notebook # -statusbar self.toplevel_vbox = gtk.VBox() self.add(self.toplevel_vbox) self.create_menu_and_toolbar() self.g_notebook = gtk.Notebook() self.toplevel_vbox.pack_start(self.g_notebook) self.vbox = gtk.VBox() self.toplevel_vbox.pack_start(self.vbox) self.create_mainwin_ui() self.show_all() def create_mainwin_ui(self): qbox = gu.hig_dlg_vbox() self.g_notebook.append_page(qbox, gtk.Label(_("Questions"))) gu.bLabel(qbox, _("Enter new chords using the mouse"), False, False) hbox = gu.bHBox(qbox, False, False) self.g_displayer = mpd.musicdisplayer.ChordEditor() self.g_displayer.connect('clicked', self.on_displayer_clicked) self.g_displayer.clear(2) gu.bLabel(hbox, "") hbox.pack_start(self.g_displayer, False) gu.bLabel(hbox, "") ## self.g_question_name = gtk.Entry() qbox.pack_start(gu.hig_label_widget(_("Question title:"), self.g_question_name, None), False) self.g_navinfo = gtk.Label("") qbox.pack_start(self.g_navinfo, False) ## self.m_P = EditorLessonfile() cvbox = gtk.VBox() self.g_notebook.append_page(cvbox, gtk.Label(_("Lessonfile header"))) ## Header section sizegroup = gtk.SizeGroup(gtk.SIZE_GROUP_HORIZONTAL) self.g_title = gtk.Entry() cvbox.pack_start(gu.hig_label_widget(_("File title:"), self.g_title, sizegroup)) self.g_content_chord = gtk.RadioButton(None, "chord") self.g_content_chord_voicing = gtk.RadioButton(self.g_content_chord, "chord-voicing") self.g_content_idbyname = gtk.RadioButton(self.g_content_chord, "id-by-name") box = gtk.HBox() box.pack_start(self.g_content_chord) box.pack_start(self.g_content_chord_voicing) box.pack_start(self.g_content_idbyname) cvbox.pack_start(gu.hig_label_widget(_("Content:"), box, sizegroup)) self.g_random_transpose = gtk.Entry() cvbox.pack_start(gu.hig_label_widget(_("Random transpose:"), self.g_random_transpose, sizegroup)) # #self.g_statusbar = gtk.Statusbar() #self.toplevel_vbox.pack_start(self.g_statusbar, False) self.update_appwin() def proceed_if_changed(self): if not self.m_P.m_changed: return True dialog = gtk.MessageDialog(self, gtk.DIALOG_MODAL, gtk.MESSAGE_QUESTION, gtk.BUTTONS_YES_NO, _("You have unsaved data. Proceed anyway?")) dialog.hide() if dialog.run() == gtk.RESPONSE_YES: dialog.destroy() return True dialog.destroy() return False def update_appwin(self): self.update_score() self.set_navinfo() self.g_title.set_text(self.m_P.header.title) self.g_random_transpose.set_text(str(self.m_P.header.random_transpose)) {'chord': self.g_content_chord, 'chordvoicing': self.g_content_chord_voicing, 'idbyname': self.g_content_idbyname}[self.m_P.header.module].set_active(True) def set_navinfo(self): if self.m_P.m_filename: self.set_title(self.m_P.m_filename) else: self.set_title(_("No file")) self.g_navinfo.set_text(_("question %(idx)i of %(count)i") % { 'idx': self.m_P._idx + 1, 'count': len(self.m_P.m_questions)}) self.g_question_name.set_text(self.m_P.m_questions[self.m_P._idx]['name']) def load_file(self, filename): self.m_P = lessonfile.ChordLessonfile(filename) self.m_P.m_changed = False if self.m_P.m_questions: self.m_P._idx = 0 self.set_navinfo() else: # Do a little trick to make an empty question self.m_P.m_questions = [dataparser.Question()] self.m_P.m_questions[-1]['music'] = lessonfile.Music("", "chord") self.m_P.m_questions[-1]['name'] = "" self.m_P._idx = 0 if self.m_P.header.module not in ('idbyname', 'chord', 'chordvoicing'): dialog = gtk.MessageDialog(self, gtk.DIALOG_MODAL, gtk.MESSAGE_ERROR, gtk.BUTTONS_CLOSE, _("The exercise module '%s' is not supported yet. Cannot edit this file.") % c) dialog.run() dialog.destroy() self.m_P = EditorLessonfile() self.update_appwin() def file_open_cb(self, *v): dialog = gtk.FileChooserDialog(_("Open..."), self, gtk.FILE_CHOOSER_ACTION_OPEN, (gtk.STOCK_CANCEL, gtk.RESPONSE_CANCEL, gtk.STOCK_OPEN, gtk.RESPONSE_OK)) dialog.set_default_response(gtk.RESPONSE_OK) if dialog.run() == gtk.RESPONSE_OK: filename = dialog.get_filename() try: self.load_file(filename) except Exception, e: dialog.destroy() m = gtk.MessageDialog(self, gtk.DIALOG_MODAL, gtk.MESSAGE_ERROR, gtk.BUTTONS_CLOSE, _("Loading file '%(filename)s' failed: %(msg)s") % {'filename': filename, 'msg': e}) m.run() m.destroy() else: dialog.destroy() else: dialog.destroy() def file_new_cb(self, action, v=None): if self.proceed_if_changed(): self.m_P = EditorLessonfile() self.update_appwin() def file_save_as_cb(self, *v): self.store_data_from_ui() dialog = gtk.FileChooserDialog(_("Save as..."), self, gtk.FILE_CHOOSER_ACTION_SAVE, (gtk.STOCK_CANCEL, gtk.RESPONSE_CANCEL, gtk.STOCK_SAVE, gtk.RESPONSE_OK)) dialog.set_default_response(gtk.RESPONSE_OK) if dialog.run() == gtk.RESPONSE_OK: self.m_P.m_filename = dialog.get_filename() self.save_file() dialog.destroy() def file_save_cb(self, *v): self.store_data_from_ui() if self.m_P.m_filename is None: dialog = gtk.FileChooserDialog(_("Save..."), self, gtk.FILE_CHOOSER_ACTION_SAVE, (gtk.STOCK_CANCEL, gtk.RESPONSE_CANCEL, gtk.STOCK_SAVE, gtk.RESPONSE_OK)) dialog.set_default_response(gtk.RESPONSE_OK) if dialog.run() == gtk.RESPONSE_OK: self.m_P.m_filename = dialog.get_filename() dialog.destroy() if self.m_P.m_filename: self.update_appwin() self.save_file() def save_file(self): if not self.m_P.m_filename: raise "No filename. Cannot save." ofile = open(self.m_P.m_filename, 'w') ofile.write("# Creator: GNU Solfege lesson file editor %s\n\n" % app_version) ofile.write("header {\n module = %s\n" % self.m_P.header.module) if type(self.m_P.header.random_transpose) == list: ofile.write(" random_transpose = %s, %s, %s\n" % (self.m_P.header.random_transpose[0], self.m_P.header.random_transpose[1], self.m_P.header.random_transpose[2])) else: ofile.write(" random_transpose = yes\n") if self.m_P.header.lesson_id: ofile.write(' lesson_id = "%s"\n' % self.m_P.header.lesson_id) ofile.write(' title = "%s"\n}\n' % self.m_P.header.title) for q in self.m_P.m_questions: print >> ofile, 'question {' print >> ofile, ' name = "%s"' % q['name'] print >> ofile, ' music = music("%s", chord)' % q['music'].m_musicdata print >> ofile, '}' ofile.close() self.m_P.m_changed = False def quit_cb(self, *v): if self.proceed_if_changed(): gtk.main_quit() def help_cb(self, *v): if not self.g_help_window: self.g_help_window = HelpWindow(self) self.g_help_window.source("""

GNU Solfege lesson file editor %s

This is the very first unfinished release. Backup the files you edit, since it can screw up.

The parser can create files for the chord exercise. It can parse more advanced lesson files than it can write. So you might loose data if you edit your hand written lesson files with this program.

""" % app_version) self.g_help_window.show() else: self.g_help_window.present() def about_cb(self, *v): dialog = gtk.MessageDialog(self, gtk.DIALOG_MODAL, gtk.MESSAGE_INFO, gtk.BUTTONS_CLOSE, "GNU Solfege lesson file editor %s\nCopyright (C) 2004, 2005 Tom Cato Amundsen " % app_version) dialog.run() dialog.destroy() def goto_first_question_cb(self, *v): self.store_data_from_ui() self.m_P._idx = 0 self.update_appwin() def go_back_question_cb(self, *v): self.store_data_from_ui() self.m_P._idx = max(0, self.m_P._idx - 1) self.update_appwin() def go_forward_question_cb(self, *v): self.store_data_from_ui() self.m_P._idx = min(self.m_P._idx + 1, len(self.m_P.m_questions) - 1) self.update_appwin() def goto_last_question_cb(self, *v): self.store_data_from_ui() self.m_P._idx = len(self.m_P.m_questions) - 1 self.update_appwin() def new_question_cb(self, *v): self.store_data_from_ui() self.m_P.m_questions.append(dataparser.Question()) self.m_P.m_questions[-1]['music'] = lessonfile.Music("", "chord") self.m_P.m_questions[-1]['name'] = "" self.m_P._idx = len(self.m_P.m_questions) - 1 self.update_appwin() def select_cursor_2flat_cb(self, *v): self.g_displayer.set_cursor("-2") def select_cursor_flat_cb(self, *v): self.g_displayer.set_cursor(-1) def select_cursor_natural_cb(self, *v): self.g_displayer.set_cursor(0) def select_cursor_sharp_cb(self, *v): self.g_displayer.set_cursor("1") def select_cursor_2sharp_cb(self, *v): self.g_displayer.set_cursor("2") def select_cursor_erase_cb(self, *v): self.g_displayer.set_cursor("erase") def select_cursor_notehead_cb(self, *v): self.g_displayer.set_cursor("notehead") def update_score(self): """ Set m_chord_tones based on the data in the lesson file. Then call g_displayer.display to show the music. """ assert self.m_P self.m_chord_tones = {} for n in self.m_P.m_questions[self.m_P._idx]['music'].m_musicdata.split(): p = mpd.MusicalPitch.new_from_notename(n) self.m_chord_tones[p.steps()] = p # if self.m_chord_tones: s = "" for n in self.m_chord_tones.values(): s += " " + n.get_octave_notename() self.g_displayer.display("\staff{ < %s >}\staff{\clef bass}" % s, "20-tight") else: self.g_displayer.display("\staff{ }\staff{\clef bass}", "20-tight") self.g_displayer.set_size_request(400, -1) def store_data_from_ui(self): self.m_P.m_questions[self.m_P._idx]['name'] = self.g_question_name.get_text() self.m_P.header.title = self.g_title.get_text() self.m_P.header.random_transpose = eval(self.g_random_transpose.get_text()) if self.g_content_chord.get_active(): self.m_P.header.module = 'chord' if self.g_content_chord_voicing.get_active(): self.m_P.header.module = 'chordvoicing' if self.g_content_idbyname.get_active(): self.m_P.header.module = 'idbyname' def on_displayer_clicked(self, ed, steps): self.m_P.m_changed = True notename = ("c", "d", "e", "f", "g", "a", "b")[6-(steps % 7)] n = mpd.MusicalPitch.new_from_notename(notename) n.m_octave_i = 1-(steps // 7) if self.g_displayer.m_cursor == 'notehead': if n.steps() not in self.m_chord_tones: self.m_chord_tones[n.steps()] = n elif self.g_displayer.m_cursor == 'erase': if n.steps() in self.m_chord_tones: del self.m_chord_tones[n.steps()] else: if n.steps() not in self.m_chord_tones: return else: self.m_chord_tones[n.steps()].m_accidental_i = int(self.g_displayer.m_cursor) v = self.m_chord_tones.values() v.sort() v = [y.get_octave_notename() for y in v] self.m_P.m_questions[self.m_P._idx]['music'].m_musicdata = " ".join(v) self.update_score() class UIManagerMainWin(MainWin): def __init__(self, datadir): MainWin.__init__(self, datadir) def create_menu_and_toolbar(self): self.window_ag = gtk.ActionGroup('WindowActions') self.lessonfile_ag = gtk.ActionGroup('LessonfileActions') self.window_ag.add_actions(fix_actions(window_actions, self)) self.lessonfile_ag.add_actions(fix_actions(lessonfile_actions, self)) self.ui = gtk.UIManager() self.ui.insert_action_group(self.window_ag, 0) self.ui.insert_action_group(self.lessonfile_ag, 1) self.ui.add_ui_from_string(ui_string) self.add_accel_group(self.ui.get_accel_group()) self.toplevel_vbox.pack_start(self.ui.get_widget('/Menubar'), False) self.ui.get_widget('/Toolbar').set_style(gtk.TOOLBAR_ICONS) self.toplevel_vbox.pack_start(self.ui.get_widget('/Toolbar'), False) def main(datadir): mpd.engravers.fetadir = os.path.join(datadir, "feta") w = UIManagerMainWin(datadir) if len(sys.argv) == 2: w.load_file(sys.argv[1]) w.show() gtk.main()