1# This file is part of the Frescobaldi project, http://www.frescobaldi.org/ 2# 3# Copyright (c) 2011 - 2014 by Wilbert Berendsen 4# 5# This program is free software; you can redistribute it and/or 6# modify it under the terms of the GNU General Public License 7# as published by the Free Software Foundation; either version 2 8# of the License, or (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, write to the Free Software 17# Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA 18# See http://www.gnu.org/licenses/ for more information. 19 20""" 21Various tools to edit pitch of selected music. 22 23All use the tools in ly.pitch. 24 25""" 26 27 28import re 29 30from PyQt5.QtCore import Qt 31from PyQt5.QtWidgets import QMessageBox 32 33import app 34import icons 35import qutil 36import lydocument 37import documentinfo 38import lilypondinfo 39import inputdialog 40import ly.pitch.translate 41import ly.pitch.transpose 42import ly.pitch.rel2abs 43import ly.pitch.abs2rel 44 45 46def changeLanguage(cursor, language): 47 """Changes the language of the pitch names.""" 48 c = lydocument.cursor(cursor, select_all=True) 49 try: 50 with qutil.busyCursor(): 51 changed = ly.pitch.translate.translate(c, language) 52 except ly.pitch.PitchNameNotAvailable: 53 QMessageBox.critical(None, app.caption(_("Pitch Name Language")), _( 54 "Can't perform the requested translation.\n\n" 55 "The music contains quarter-tone alterations, but " 56 "those are not available in the pitch language \"{name}\"." 57 ).format(name=language)) 58 return 59 if changed: 60 return 61 if not cursor.hasSelection(): 62 # there was no selection and no language command, so insert one 63 version = (documentinfo.docinfo(cursor.document()).version() 64 or lilypondinfo.preferred().version()) 65 ly.pitch.translate.insert_language(c.document, language, version) 66 return 67 # there was a selection but no command, user must insert manually. 68 QMessageBox.information(None, app.caption(_("Pitch Name Language")), 69 '<p>{0}</p>' 70 '<p><code>\\include "{1}.ly"</code> {2}</p>' 71 '<p><code>\\language "{1}"</code> {3}</p>'.format( 72 _("The pitch language of the selected text has been " 73 "updated, but you need to manually add the following " 74 "command to your document:"), 75 language, 76 _("(for LilyPond below 2.14), or"), 77 _("(for LilyPond 2.14 and higher.)"))) 78 79 80def rel2abs(cursor, first_pitch_absolute): 81 """Converts pitches from relative to absolute.""" 82 with qutil.busyCursor(): 83 c = lydocument.cursor(cursor, select_all=True) 84 ly.pitch.rel2abs.rel2abs(c, first_pitch_absolute=first_pitch_absolute) 85 86 87def abs2rel(cursor, startpitch, first_pitch_absolute): 88 """Converts pitches from absolute to relative.""" 89 with qutil.busyCursor(): 90 c = lydocument.cursor(cursor, select_all=True) 91 ly.pitch.abs2rel.abs2rel(c, startpitch=startpitch, first_pitch_absolute=first_pitch_absolute) 92 93 94def getTransposer(document, mainwindow): 95 """Show a dialog and return the desired transposer. 96 97 Returns None if the dialog was cancelled. 98 99 """ 100 language = documentinfo.docinfo(document).language() or 'nederlands' 101 102 def readpitches(text): 103 """Reads pitches from text.""" 104 result = [] 105 for pitch, octave in re.findall(r"([a-z]+)([,']*)", text): 106 r = ly.pitch.pitchReader(language)(pitch) 107 if r: 108 result.append(ly.pitch.Pitch(*r, octave=ly.pitch.octaveToNum(octave))) 109 return result 110 111 def validate(text): 112 """Returns whether the text contains exactly two pitches.""" 113 return len(readpitches(text)) == 2 114 115 text = inputdialog.getText(mainwindow, _("Transpose"), _( 116 "Please enter two absolute pitches, separated by a space, " 117 "using the pitch name language \"{language}\"." 118 ).format(language=language), icon = icons.get('tools-transpose'), 119 help = "transpose", validate = validate) 120 121 if text: 122 return ly.pitch.transpose.Transposer(*readpitches(text)) 123 124 125def getModalTransposer(document, mainwindow): 126 """Show a dialog and return the desired modal transposer. 127 128 Returns None if the dialog was cancelled. 129 130 """ 131 language = documentinfo.docinfo(document).language() or 'nederlands' 132 133 def readpitches(text): 134 """Reads pitches from text.""" 135 result = [] 136 for pitch, octave in re.findall(r"([a-z]+)([,']*)", text): 137 r = ly.pitch.pitchReader(language)(pitch) 138 if r: 139 result.append(ly.pitch.Pitch(*r, octave=ly.pitch.octaveToNum(octave))) 140 return result 141 142 def validate(text): 143 """Returns whether the text is an integer followed by the name of a key.""" 144 words = text.split() 145 if len(words) != 2: 146 return False 147 try: 148 steps = int(words[0]) 149 keyIndex = ly.pitch.transpose.ModalTransposer.getKeyIndex(words[1]) 150 return True 151 except ValueError: 152 return False 153 154 text = inputdialog.getText(mainwindow, _("Transpose"), _( 155 "Please enter the number of steps to alter by, followed by a key signature. (i.e. \"5 F\")" 156 ), icon = icons.get('tools-transpose'), 157 help = "modal_transpose", validate = validate) 158 if text: 159 words = text.split() 160 return ly.pitch.transpose.ModalTransposer(int(words[0]), ly.pitch.transpose.ModalTransposer.getKeyIndex(words[1])) 161 162 163def getModeShifter(document, mainwindow): 164 """Show a dialog and return the desired mode shifter. 165 166 Returns None if the dialog was cancelled. 167 168 """ 169 language = documentinfo.docinfo(document).language() or 'nederlands' 170 171 def readpitches(text): 172 """Reads pitches from text.""" 173 result = [] 174 for pitch, octave in re.findall(r"([a-z]+)([,']*)", text.lower()): 175 r = ly.pitch.pitchReader(language)(pitch) 176 if r: 177 result.append(ly.pitch.Pitch(*r, octave=ly.pitch.octaveToNum(octave))) 178 return result 179 180 def validate(text): 181 """Validates text by checking if it contains a defined mode.""" 182 return len(readpitches(text)) == 1 183 184 from . import dialog 185 dlg = dialog.ModeShiftDialog(mainwindow) 186 dlg.addAction(mainwindow.actionCollection.help_whatsthis) 187 dlg.setWindowModality(Qt.WindowModal) 188 dlg.setKeyValidator(validate) 189 if dlg.exec_(): 190 key, scale = dlg.getMode() 191 key = readpitches(key)[0] 192 dlg.saveSettings() 193 return ly.pitch.transpose.ModeShifter(key, scale) 194 195 196def transpose(cursor, transposer, mainwindow=None, relative_first_pitch_absolute=False): 197 """Transpose pitches using the specified transposer.""" 198 c = lydocument.cursor(cursor, select_all=True) 199 try: 200 with qutil.busyCursor(): 201 ly.pitch.transpose.transpose(c, transposer, 202 relative_first_pitch_absolute=relative_first_pitch_absolute) 203 except ly.pitch.PitchNameNotAvailable as e: 204 QMessageBox.critical(mainwindow, app.caption(_("Transpose")), _( 205 "Can't perform the requested transposition.\n\n" 206 "The transposed music would contain quarter-tone alterations " 207 "that are not available in the pitch language \"{language}\"." 208 ).format(language = e.language)) 209 210 211