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