# GNU Solfege - free ear training software # Copyright (C) 2000, 2001, 2002, 2003, 2004, 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 import time import gtk import gobject import re import soundcard import cfg import os PAD = 8 PAD_SMALL = 4 # Prefixes used in this module: # t pack into a table, the first five parameters are table, x1, x2, y1, y2 # n a widget that get its state stored in ~/.solfegerc # b the widget is packed into the first argument def tLabel(table, x1, x2, y1, y2, text="", xalign=0.0, yalign=0.5, xoptions=gtk.EXPAND|gtk.FILL, yoptions=gtk.EXPAND|gtk.FILL, xpadding=0, ypadding=0): label = gtk.Label(text) label.set_alignment(xalign, yalign) table.attach(label, x1, x2, y1, y2, xoptions=xoptions, yoptions=yoptions, xpadding=xpadding, ypadding=ypadding) return label def bLabel(pack_into, label, expand=True, fill=True): b = gtk.Label(label) b.show() pack_into.pack_start(b, expand, fill) return b def bButton(pack_into, label, callback=None, expand=True, fill=True): b = gtk.Button(label) b.show() if callback: b.connect('clicked', callback) pack_into.pack_start(b, expand, fill) return b class nSpinButton(gtk.SpinButton, cfg.ConfigUtils):#FIXME (??what is there to fix???) def __init__(self, exname, name, adj, climb_rate=1, digits=1): gtk.SpinButton.__init__(self, adj, climb_rate, digits) cfg.ConfigUtils.__init__(self, exname) self.m_name = name self.set_digits(0) self.show() self.set_value(self.get_float(self.m_name)) if self.get_value() != self.get_float(self.m_name): self.set_float(self.m_name, self.get_value()) self.connect('value-changed', self.on_changed) self._watch_id = self.add_watch(self.m_name, self._watch_cb) self.m_stop_watch = 0 def _watch_cb(self, name): if not self.m_stop_watch: gtk.SpinButton.set_value(self, self.get_float(name)) def set_value(self, value): gtk.SpinButton.set_value(self, value) self.set_float(self.m_name, value) def on_changed(self, _o): self.m_stop_watch = 1 self.set_float(self.m_name, self.get_value()) self.m_stop_watch = 0 def tSpinButton(table, x1, x2, y1, y2, value, lower, upper, step_incr=1, page_incr=10, callback=None): adj = gtk.Adjustment(value, lower, upper, step_incr, page_incr) spin = gtk.SpinButton(adj, digits=0) if callback: spin.connect('value-changed', callback) table.attach(spin, x1, x2, y1, y2) return spin def bHBox(pack_into, expand=True, fill=True, padding=0): b = gtk.HBox() b.show() pack_into.pack_start(b, expand, fill, padding) return b def bVBox(pack_into, expand=True, fill=True, padding=0): b = gtk.VBox() pack_into.pack_start(b, expand, fill, padding) return b class nCheckButton(gtk.CheckButton, cfg.ConfigUtils): def __init__(self, exname, name, label=None, default_value=0, callback=None): gtk.CheckButton.__init__(self, label) #cfg.ConfigUtils.__init__(self, exname) cfg.ConfigUtils.__dict__['__init__'](self, exname) self.m_name = name self.m_callback = callback self.show() if default_value: s = "true" else: s = "false" self.set_bool(self.m_name, self.get_bool(self.m_name+"="+s)) if self.get_bool(self.m_name): self.set_active(1) self._clicked_id = self.connect('toggled', self.on_clicked) self._watch_id = self.add_watch(self.m_name, self._watch_cb) def _watch_cb(self, name): self.set_active(self.get_bool(name)) def on_clicked(self, _o): self.set_bool(self.m_name, self.get_active()) if self.m_callback: self.m_callback(_o) def RadioButton(group, label, callback=None): rdb = gtk.RadioButton(group, label) if callback: rdb.connect('toggled', callback) rdb.show() return rdb class nCombo(gtk.Combo, cfg.ConfigUtils): def __init__(self, exname, name, default, popdown_strings): """ Be aware that the value of the entry, is stored as an integer popdown_strings.index(entry.get_text()), so if popdown_strings changes when upgrading the program, the value of the combo might change. Despite this problems, I do it this way, because if we store the actual value of the entry, we get into trouble when running the program with other locale settings. """ gtk.Combo.__init__(self) #cfg.ConfigUtils.__init__(self, exname) cfg.ConfigUtils.__dict__['__init__'](self, exname) self.popdown_strings = popdown_strings self.m_name = name self.set_value_in_list(True, False) self.set_popdown_strings(popdown_strings) i = self.get_int_with_default(name, -1) if i == -1: i = popdown_strings.index(default) self.entry.set_text(popdown_strings[i]) self.entry.connect("changed", self.entry_changed) self.entry.set_editable(False) self.show() def entry_changed(self, entry): self.set_int(self.m_name, self.popdown_strings.index(entry.get_text())) class PercussionNameComboBoxEntry(gtk.ComboBoxEntry, cfg.ConfigUtils): def __init__(self, exname, name, default): liststore = gtk.ListStore(gobject.TYPE_STRING) for pn in soundcard.percussion_names: liststore.append((pn,)) gtk.ComboBoxEntry.__init__(self, liststore) cfg.ConfigUtils.__init__(self, exname) self.m_name = name i = self.get_int(name) if not i: i = soundcard.percussionname_to_int(default) self.set_int(name, i) self.child.set_text(soundcard.int_to_percussionname(i)) self.connect("changed", self.entry_changed) def entry_changed(self, widget): #FIXME value 35 should be taken from soundcard module. self.set_int(self.m_name, widget.get_active()+35) class FlashBar(gtk.Frame): def __init__(self): gtk.Frame.__init__(self) self.set_shadow_type(gtk.SHADOW_IN) #FIXME different gtk themes can make these values wrong self.set_size_request(-1, 40) self.__stack = [] self.__label = HarmonicProgressionLabel('') self.add(self.__label) self.__timeout = None # The allocated size self.m_sx, self.m_sy = 0, 0 def require_size(self, stringlist): """ stringlist is a list of the strings believed to be widest. require_size will make sure that the flashbar is at least large enough to show all the strings in stringlist. """ for s in stringlist: self.__label.set_text(s) x, y = self.__label.size_request() self.m_sx = max(x, self.m_sx) self.m_sy = max(y, self.m_sy) self.__label.set_text("") self.__label.set_size_request(self.m_sx, self.m_sy) def flash(self, txt): """Display a message that is automatically removed after some time. If we flash a new message before the old flashed message are removed, we old flashed message are removed. """ if self.__timeout: gobject.source_remove(self.__timeout) self.__label.set_size_request(-1, -1) self.__label.set_text(txt) sx, sy = self.__label.size_request() self.m_sx = max(sx, self.m_sx) self.m_sy = max(sy, self.m_sy) self.__label.set_size_request(self.m_sx, self.m_sy) def f(self=self): self.__timeout = None if self.__stack: self.__label.set_text(self.__stack[-1]) else: self.__label.set_text('') self.__timeout = gobject.timeout_add(2000, f) def push(self, txt): # stop any flashing before we push if self.__timeout: gobject.source_remove(self.__timeout) self.__timeout = None self.__stack.append(txt) self.__label.set_text(txt, '') def pop(self): """If a message is being flashed right now, that flashing is not affected, but the message below the flashed message is removed. """ if self.__stack: self.__stack.pop() if not self.__timeout: if self.__stack: self.__label.set_text(self.__stack[-1]) else: self.__label.set_text('') def clear(self): self.__stack = [] if not self.__timeout: self.__label.set_text('') def set(self, txt): """ Empty the stack of messages and display the string in txt. """ self.__stack = [txt] self.__label.set_text(txt) def hig_dlg_vbox(): """a GtkVBox containing as many rows as you wish to have categories inside the control area of the GtkDialog. """ vbox = gtk.VBox() vbox.set_spacing(18) vbox.set_border_width(12) return vbox def hig_category_vbox(title): """ Return a tuple of two boxes: box1 -- a box containing everything including the title. Useful if you have to hide a category. box2 The box you should pack your stuff in. """ vbox = gtk.VBox() vbox.set_spacing(6) label = gtk.Label('%s' % title) label.set_use_markup(True) label.set_alignment(0.0, 0.0) vbox.pack_start(label, False) hbox = gtk.HBox() vbox.pack_start(hbox, False) fill = gtk.Label(" ") hbox.pack_start(fill, False) category_content_vbox = gtk.VBox() hbox.pack_start(category_content_vbox, True) category_content_vbox.set_spacing(6) vbox.show_all() return vbox, category_content_vbox def hig_label_widget(txt, widget, sizegroup): """ Return a box containing a label and a widget, aligned nice as the HIG say we should. """ hbox = gtk.HBox() hbox.set_spacing(6) label = gtk.Label(txt) label.set_alignment(0.0, 0.5) if sizegroup: sizegroup.add_widget(label) hbox.pack_start(label, False) if type(widget) != type([]): widget = [widget] for w in widget: hbox.pack_start(w,False) label.set_use_underline(True) label.set_mnemonic_widget(widget[0]) return hbox class SpinButtonRangeController(object): def __init__(self, spin_low, spin_high, lowest_value, highest_value): self.g_spin_low = spin_low self.g_spin_low.connect('value-changed', self.on_low_changed) self.g_spin_high = spin_high self.g_spin_high.connect('value-changed', self.on_high_changed) self.m_lowest_value = lowest_value self.m_highest_value = highest_value def on_low_changed(self, widget, *v): if widget.get_value() > self.g_spin_high.get_value(): self.g_spin_low.set_value(self.g_spin_high.get_value()) elif widget.get_value() < self.m_lowest_value: self.g_spin_low.set_value(self.m_lowest_value) def on_high_changed(self, widget, *v): if widget.get_value() < self.g_spin_low.get_value(): self.g_spin_high.set_value(self.g_spin_low.get_value()) elif widget.get_value() > self.m_highest_value: self.g_spin_high.set_value(self.m_highest_value) def create_stock_menu_item(stock, txt, callback, ag, accel_key, accel_mod): box = gtk.HBox() box.set_spacing(gu.PAD_SMALL) im = gtk.Image() im.set_from_stock(stock, gtk.ICON_SIZE_MENU) item = gtk.ImageMenuItem(txt) item.set_image(im) if accel_key != 0: item.add_accelerator('activate', ag, accel_key, accel_mod, gtk.ACCEL_VISIBLE) item.connect('activate', callback) return item class HarmonicProgressionLabel(gtk.HBox): """ This class can parse strings like I-(6,4)V(5,3)-I and can be used as button labels. """ def __init__(self, str, align=''): gtk.HBox.__init__(self) self.show() self.set_text(str, align) def set_text(self, str, align=''): for o in self.get_children(): o.destroy() self.m_str = str if align == 'center': self.pack_start(gtk.HBox()) while self.m_str: T, A, B = self.get_next_token() if T == 'big' or T == 'err': self.bigchar(A) elif T == 'two': self.twoline(A, B) else: assert T == 'one' self.oneline(A) if align == 'center': self.pack_start(gtk.HBox()) self.show_all() def get_next_token(self): m_re1 = re.compile("([^\(]+)", re.UNICODE) m_re2 = re.compile("\((\w*),\s*(\w*)\)", re.UNICODE) m_re3 = re.compile("\((\w*)\)", re.UNICODE) m1 = m_re1.match(self.m_str) m2 = m_re2.match(self.m_str) m3 = m_re3.match(self.m_str) if m1: self.m_str = self.m_str[len(m1.group()):] return "big", m1.groups()[0], None if m2: self.m_str = self.m_str[len(m2.group()):] return "two", m2.groups()[0], m2.groups()[1] if m3: self.m_str = self.m_str[len(m3.group()):] return "one", m3.groups()[0], None return "err" def twoline(self, A, B): vbox = gtk.VBox() t1 = gtk.Label(A) t1.set_name("ProgressionLabelNumber") t1.show();vbox.pack_start(t1); t1.set_alignment(0, 0); t2 = gtk.Label(B) t2.set_name("ProgressionLabelNumber") t2.show();vbox.pack_start(t2); t2.set_alignment(0, 0); self.pack_start(vbox, False) def oneline(self, A): vbox = gtk.VBox() t = gtk.Label(A) t.set_name("ProgressionLabelNumber") t.show();vbox.pack_start(t); t.set_alignment(0, 0); self.pack_start(vbox, False) def bigchar(self, A): t1 = gtk.Label(A) t1.set_name("ProgressionNameLabel") t1.show() self.pack_start(t1, False) def dialog_yesno(text, parent=None): """Return True if the answer is yes, False if the answer is no. """ m = gtk.MessageDialog(parent, gtk.DIALOG_MODAL, gtk.MESSAGE_QUESTION, gtk.BUTTONS_YES_NO, text) ret = m.run() m.destroy() return ret == gtk.RESPONSE_YES def dialog_ok(text, parent=None, secondary_text=None, msgtype=gtk.MESSAGE_INFO): """" Return the gtk.RESPONSE_XXXX returned by .run() """ m = gtk.MessageDialog(parent, gtk.DIALOG_MODAL, msgtype, gtk.BUTTONS_OK, text) if secondary_text: m.format_secondary_text(secondary_text) ret = m.run() m.destroy() return ret class NewLineBox(gtk.VBox): def __init__(self): gtk.VBox.__init__(self) self.m_todo_widgets = [] def add_widget(self, widget): self.m_todo_widgets.append(widget) def show_widgets(self): if 'newline' in self.m_todo_widgets: self._newline_show_widgets() else: self._flow_show_widgets() def newline(self): self.m_todo_widgets.append('newline') def _newline_show_widgets(self): sizegroup = gtk.SizeGroup(gtk.SIZE_GROUP_HORIZONTAL) hbox = bHBox(self, True) for n in self.m_todo_widgets: if n == 'newline': hbox = bHBox(self, False) else: hbox.pack_start(n, False) sizegroup.add_widget(n) def _flow_show_widgets(self): w = 8 num_lines = len(self.m_todo_widgets) // w + 1 w = len(self.m_todo_widgets) // num_lines + 1 sizegroup = gtk.SizeGroup(gtk.SIZE_GROUP_HORIZONTAL) c = 0 for n in self.m_todo_widgets: sizegroup.add_widget(n) if c % w == 0: hbox = bHBox(self, True, True) hbox.pack_start(n, False) c += 1 self.show_all() def empty(self): self.foreach(lambda w: w.destroy()) self.m_todo_widgets = [] def get_max_child_height(self): return max([c.size_request()[1] for c in self.m_todo_widgets]) def create_png_image(fn): """ Create an image by loading a png file from graphics dir """ im = gtk.Image() im.set_from_file(os.path.join('graphics', fn)+'.png') im.show() return im def create_rhythm_image(rhythm): """ rhythm : a string like 'c8 c8' or 'c8 c16 c16' The image returned is shown. """ im = gtk.Image() im.set_from_file(os.path.join('graphics', 'rhythm-%s.png' % (rhythm.replace(" ", "")))) im.show() return im class EditorDialogBase(gtk.Window): # Classes inheriting from this must define self.savedir instance_counter = 0 def __init__(self): gtk.Window.__init__(self) self.m_filename = None self.m_instance_number = EditorDialogBase.instance_counter EditorDialogBase.instance_counter += 1 self.connect('delete_event', self.on_delete_event) self.g_ui_manager = gtk.UIManager() self.g_actiongroup = gtk.ActionGroup('TrainingSetActions') self.g_actiongroup.add_actions([ ('Close', gtk.STOCK_CLOSE, None, None, None, self.close_window), ('Save', gtk.STOCK_SAVE, None, None, None, self.on_save), ('SaveAs', gtk.STOCK_SAVE_AS, None, None, None, self.on_save_as), ('New', gtk.STOCK_NEW, None, None, None, self.new_file), ('Open', gtk.STOCK_OPEN, None, None, None, self.on_open), ('Help', gtk.STOCK_HELP, None, None, None, self.on_show_help), ]) def _get_a_filename(self): """ Return a file name. UntitledN (where N is an integer) if we have to real name. """ if not self.m_filename: return _("Untitled%s") % EditorDialogBase.instance_counter return self.m_filename def close_window(self, *w): if not self.m_changed: self.destroy() return m = gtk.MessageDialog(self, gtk.DIALOG_MODAL, gtk.MESSAGE_WARNING, gtk.BUTTONS_NONE, _("Save changes to \"%s\" before closing?") % self._get_a_filename()) t = time.time() - self.m_savetime if t < 60: msg = _("If you don't save, changes from the past %i seconds will be permanently lost.") % int(time.time() - self.m_savetime) else: msg = _("If you don't save, changes from the past %i minutes will be permanently lost.") % int((time.time() - self.m_savetime) / 60.0) m.format_secondary_text(msg) m.add_button(_("_Close without Saving"), gtk.RESPONSE_CLOSE) m.add_button("gtk-cancel", gtk.RESPONSE_CANCEL) m.add_button("gtk-save", gtk.RESPONSE_OK) m.set_default_response(gtk.RESPONSE_OK) r = m.run() m.destroy() if r == gtk.RESPONSE_CLOSE: self.destroy() elif r in (gtk.RESPONSE_CANCEL, gtk.RESPONSE_DELETE_EVENT): return else: assert r == gtk.RESPONSE_OK self.on_save() self.destroy() def on_delete_event(self, *ignore): self.close_window() return True def on_open(self, widget): """ Open a FileChooserDialog and select a file. Load into this editor window if it is empty and unused, if not load into a new one. This method will catch exceptions raised while loading the file and display a dialog. Then it will destroy any newly created dialog. """ dialog = gtk.FileChooserDialog(None, self, gtk.FILE_CHOOSER_ACTION_OPEN, (gtk.STOCK_CANCEL, gtk.RESPONSE_CANCEL, gtk.STOCK_OK, gtk.RESPONSE_OK)) dialog.set_current_folder(self.savedir) ret = dialog.run() if ret == gtk.RESPONSE_OK: try: if (not self.m_filename) and (not self.m_changed): win = self.new_file() win.load_file(dialog.get_filename()) win.set_title(win.m_filename) self.destroy() else: win = self.new_file() win.load_file(dialog.get_filename()) win.set_title(win.m_filename) except Exception, e: # Since we catch all sorts of exceptions here, we don't have # to do handle string to int conversion in # PractiseSheet.parse_file. Just let int() raise ValueException # if the string in not an integer. self.m_app.m_ui.display_exception_message(e, __file__) win.destroy() dialog.destroy() def on_save_as(self, widget): """ Return True if the file was saved, False if not. """ dialog = gtk.FileChooserDialog(_("Save As..."), self, gtk.FILE_CHOOSER_ACTION_SAVE, (gtk.STOCK_CANCEL, gtk.RESPONSE_CANCEL, gtk.STOCK_OK, gtk.RESPONSE_OK)) dialog.set_default_response(gtk.RESPONSE_OK) if not os.path.exists(self.savedir): try: os.mkdir(self.savedir) except OSError, e: pass # if self.m_filename is set to a real filename, we will # display the folder where that file exists. If not, we will # use .solfege/trainingsets if os.path.dirname(self._get_a_filename()): dialog.set_current_folder(os.path.dirname(self._get_a_filename())) else: dialog.set_current_folder(self.savedir) if dialog.get_current_folder() == os.path.dirname(self._get_a_filename()): dialog.set_current_name(os.path.basename(self._get_a_filename())) else: dialog.set_current_name(self._get_a_filename()) while 1: ret = dialog.run() if ret == gtk.RESPONSE_OK: if os.path.exists(dialog.get_filename()): if not dialog_yesno(_("File exists. Overwrite?")): continue try: self.m_filename = dialog.get_filename() self.save() self.set_title(self.m_filename) except IOError, e: dialog_ok(_("Error saving file"), self, str(e), gtk.MESSAGE_ERROR) dialog.destroy() return False break elif ret in (gtk.RESPONSE_CANCEL, gtk.RESPONSE_DELETE_EVENT): break dialog.destroy() return ret == gtk.RESPONSE_OK def on_save(self, widget=None): """ Return True if the file was saved, False if not. """ if not self.m_filename: return self.on_save_as(widget) else: self.save() return True def new_file(self, action=None): """ Return the new dialog window. """ m = self.__class__(self.m_app) m.show_all() return m def select_empty_directory(self, title): msg = _("Select an empty directory, since we want to fill it with files.") dialog = gtk.FileChooserDialog(title, self, gtk.FILE_CHOOSER_ACTION_SELECT_FOLDER, (gtk.STOCK_CANCEL, gtk.RESPONSE_CANCEL, gtk.STOCK_OK, gtk.RESPONSE_OK)) label = gtk.Label(msg) label.show() dialog.vbox.pack_start(label, False, False) while 1: res = dialog.run() if res in (gtk.RESPONSE_CANCEL, gtk.RESPONSE_DELETE_EVENT): dialog.destroy() return elif res == gtk.RESPONSE_OK: if os.listdir(dialog.get_filename()): msg_dlg = gtk.MessageDialog(self, gtk.DIALOG_MODAL, gtk.MESSAGE_INFO, gtk.BUTTONS_OK, msg) msg_dlg.run() msg_dlg.destroy() else: break ret = dialog.get_filename() dialog.destroy() return ret