~ubuntu-branches/ubuntu/vivid/frescobaldi/vivid

« back to all changes in this revision

Viewing changes to frescobaldi_app/pitch/pitch.py

  • Committer: Package Import Robot
  • Author(s): Ryan Kavanagh
  • Date: 2012-01-03 16:20:11 UTC
  • mfrom: (1.4.1)
  • Revision ID: package-import@ubuntu.com-20120103162011-tsjkwl4sntwmprea
Tags: 2.0.0-1
* New upstream release 
* Drop the following uneeded patches:
  + 01_checkmodules_no_python-kde4_build-dep.diff
  + 02_no_pyc.diff
  + 04_no_binary_lilypond_upgrades.diff
* Needs new dependency python-poppler-qt4
* Update debian/watch for new download path
* Update copyright file with new holders and years

Show diffs side-by-side

added added

removed removed

Lines of Context:
 
1
# This file is part of the Frescobaldi project, http://www.frescobaldi.org/
 
2
#
 
3
# Copyright (c) 2011 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
"""
 
21
Implementation of the tools to edit pitch of selected music.
 
22
"""
 
23
 
 
24
from __future__ import unicode_literals
 
25
 
 
26
import itertools
 
27
import re
 
28
 
 
29
from PyQt4.QtGui import QMessageBox, QTextCursor
 
30
 
 
31
import app
 
32
import help
 
33
import icons
 
34
import ly.pitch
 
35
import ly.lex.lilypond
 
36
import cursortools
 
37
import util
 
38
import tokeniter
 
39
import documentinfo
 
40
import lilypondinfo
 
41
import inputdialog
 
42
 
 
43
 
 
44
def changeLanguage(cursor, language):
 
45
    """Changes the language of the pitch names."""
 
46
    selection = cursor.hasSelection()
 
47
    if selection:
 
48
        start = cursor.selectionStart()
 
49
        cursor.setPosition(cursor.selectionEnd())
 
50
        cursor.setPosition(0, QTextCursor.KeepAnchor)
 
51
        source = tokeniter.Source.selection(cursor)
 
52
    else:
 
53
        source = tokeniter.Source.document(cursor)
 
54
    
 
55
    pitches = PitchIterator(source)
 
56
    tokens = pitches.tokens()
 
57
    writer = ly.pitch.pitchWriter(language)
 
58
    
 
59
    if selection:
 
60
        # consume tokens before the selection, following the language
 
61
        source.consume(tokens, start)
 
62
    
 
63
    changed = False # track change of \language or \include language command
 
64
    with cursortools.editBlock(cursor):
 
65
        try:
 
66
            with util.busyCursor():
 
67
                with cursortools.Editor() as e:
 
68
                    for t in tokens:
 
69
                        if isinstance(t, ly.lex.lilypond.Note):
 
70
                            # translate the pitch name
 
71
                            p = pitches.read(t)
 
72
                            if p:
 
73
                                n = writer(*p)
 
74
                                if n != t:
 
75
                                    e.insertText(source.cursor(t), n)
 
76
                        elif isinstance(t, LanguageName) and t != language:
 
77
                            # change the language name in a command
 
78
                            e.insertText(source.cursor(t), language)
 
79
                            changed = True
 
80
        except ly.pitch.PitchNameNotAvailable:
 
81
            QMessageBox.critical(None, app.caption(_("Pitch Name Language")), _(
 
82
                "Can't perform the requested translation.\n\n"
 
83
                "The music contains quarter-tone alterations, but "
 
84
                "those are not available in the pitch language \"{name}\"."
 
85
                ).format(name=language))
 
86
            return
 
87
        if changed:
 
88
            return
 
89
        if not selection:
 
90
            # there was no selection and no language command, so insert one
 
91
            insertLanguage(cursor.document(), language)
 
92
            return
 
93
    # there was a selection but no command, user must insert manually.
 
94
    QMessageBox.information(None, app.caption(_("Pitch Name Language")),
 
95
        '<p>{0}</p>'
 
96
        '<p><code>\\include "{1}.ly"</code> {2}</p>'
 
97
        '<p><code>\\language "{1}"</code> {3}</p>'.format(
 
98
            _("The pitch language of the selected text has been "
 
99
                "updated, but you need to manually add the following "
 
100
                "command to your document:"),
 
101
            language,
 
102
            _("(for LilyPond below 2.14), or"),
 
103
            _("(for LilyPond 2.14 and higher.)")))
 
104
 
 
105
 
 
106
def insertLanguage(document, language):
 
107
    """Inserts a language command in the document.
 
108
    
 
109
    The command is inserted at the top or just below the version line.
 
110
    If the document uses LilyPond < 2.13.38, the \\include command is used,
 
111
    otherwise the newer \\language command.
 
112
    
 
113
    """
 
114
    version = (documentinfo.info(document).version()
 
115
               or lilypondinfo.preferred().version)
 
116
    if version and version < (2, 13, 38):
 
117
        text = '\\include "{0}.ly"'
 
118
    else:
 
119
        text = '\\language "{0}"'
 
120
    # insert language command on top of file, but below version
 
121
    block = document.firstBlock()
 
122
    c = QTextCursor(block)
 
123
    if '\\version' in tokeniter.tokens(block):
 
124
        c.movePosition(QTextCursor.EndOfBlock)
 
125
        text = '\n' + text
 
126
    else:
 
127
        text += '\n'
 
128
    c.insertText(text.format(language))
 
129
 
 
130
 
 
131
def rel2abs(cursor):
 
132
    """Converts pitches from relative to absolute."""
 
133
    selection = cursor.hasSelection()
 
134
    if selection:
 
135
        start = cursor.selectionStart()
 
136
        cursor.setPosition(cursor.selectionEnd())
 
137
        cursor.setPosition(0, QTextCursor.KeepAnchor)
 
138
        source = tokeniter.Source.selection(cursor, True)
 
139
    else:
 
140
        source = tokeniter.Source.document(cursor, True)
 
141
    
 
142
    pitches = PitchIterator(source)
 
143
    psource = pitches.pitches()
 
144
    if selection:
 
145
        # consume tokens before the selection, following the language
 
146
        t = source.consume(pitches.tokens(), start)
 
147
        if t:
 
148
            psource = itertools.chain((t,), psource)
 
149
    
 
150
    # this class dispatches the tokens. we can't use a generator function
 
151
    # as that doesn't like to be called again while there is already a body
 
152
    # running.
 
153
    class gen(object):
 
154
        def __iter__(self):
 
155
            return self
 
156
        
 
157
        def __next__(self):
 
158
            t = next(psource)
 
159
            while isinstance(t, (ly.lex.Space, ly.lex.Comment)):
 
160
                t = next(psource)
 
161
            if t == '\\relative' and isinstance(t, ly.lex.lilypond.Command):
 
162
                relative(t)
 
163
                t = next(psource)
 
164
            elif isinstance(t, ly.lex.lilypond.MarkupScore):
 
165
                consume()
 
166
                t = next(psource)
 
167
            return t
 
168
        
 
169
        next = __next__
 
170
            
 
171
    tsource = gen()
 
172
    
 
173
    def makeAbsolute(p, lastPitch):
 
174
        """Makes pitch absolute (honoring and removing possible octaveCheck)."""
 
175
        if p.octaveCheck is not None:
 
176
            p.octave = p.octaveCheck
 
177
            p.octaveCheck = None
 
178
        else:
 
179
            p.makeAbsolute(lastPitch)
 
180
        pitches.write(p, editor)
 
181
    
 
182
    def context():
 
183
        """Consume tokens till the level drops (we exit a construct)."""
 
184
        depth = source.state.depth()
 
185
        for t in tsource:
 
186
            yield t
 
187
            if source.state.depth() < depth:
 
188
                return
 
189
    
 
190
    def consume():
 
191
        """Consume tokens from context() returning the last token, if any."""
 
192
        t = None
 
193
        for t in context():
 
194
            pass
 
195
        return t
 
196
    
 
197
    def relative(t):
 
198
        c = source.cursor(t)
 
199
        lastPitch = None
 
200
        
 
201
        t = next(tsource)
 
202
        if isinstance(t, Pitch):
 
203
            lastPitch = t
 
204
            t = next(tsource)
 
205
        else:
 
206
            lastPitch = Pitch.c1()
 
207
        
 
208
        # remove the \relative <pitch> tokens
 
209
        c.setPosition(source.position(t), c.KeepAnchor)
 
210
        editor.removeSelectedText(c)
 
211
        
 
212
        while True:
 
213
            # eat stuff like \new Staff == "bla" \new Voice \notes etc.
 
214
            if isinstance(source.state.parser(), ly.lex.lilypond.ParseNewContext):
 
215
                t = consume()
 
216
            elif isinstance(t, (ly.lex.lilypond.ChordMode, ly.lex.lilypond.NoteMode)):
 
217
                t = next(tsource)
 
218
            else:
 
219
                break
 
220
        
 
221
        # now convert the relative expression to absolute
 
222
        if t in ('{', '<<'):
 
223
            # Handle full music expression { ... } or << ... >>
 
224
            for t in context():
 
225
                # skip commands with pitches that do not count
 
226
                if isinstance(t, ly.lex.lilypond.PitchCommand):
 
227
                    if t == '\\octaveCheck':
 
228
                        c = source.cursor(t)
 
229
                        for p in getpitches(context()):
 
230
                            # remove the \octaveCheck
 
231
                            lastPitch = p
 
232
                            c.setPosition((p.octaveCursor or p.noteCursor).selectionEnd(), c.KeepAnchor)
 
233
                            editor.removeSelectedText(c)
 
234
                            break
 
235
                    else:
 
236
                        consume()
 
237
                elif isinstance(t, ly.lex.lilypond.ChordStart):
 
238
                    # handle chord
 
239
                    chord = [lastPitch]
 
240
                    for p in getpitches(context()):
 
241
                        makeAbsolute(p, chord[-1])
 
242
                        chord.append(p)
 
243
                    lastPitch = chord[:2][-1] # same or first
 
244
                elif isinstance(t, Pitch):
 
245
                    makeAbsolute(t, lastPitch)
 
246
                    lastPitch = t
 
247
        elif isinstance(t, ly.lex.lilypond.ChordStart):
 
248
            # Handle just one chord
 
249
            for p in getpitches(context()):
 
250
                makeAbsolute(p, lastPitch)
 
251
                lastPitch = p
 
252
        elif isinstance(t, Pitch):
 
253
            # Handle just one pitch
 
254
            makeAbsolute(t, lastPitch)
 
255
    
 
256
    # Do it!
 
257
    with util.busyCursor():
 
258
        with cursortools.Editor() as editor:
 
259
            for t in tsource:
 
260
                pass
 
261
 
 
262
 
 
263
def abs2rel(cursor):
 
264
    """Converts pitches from absolute to relative."""
 
265
    selection = cursor.hasSelection()
 
266
    if selection:
 
267
        start = cursor.selectionStart()
 
268
        cursor.setPosition(cursor.selectionEnd())
 
269
        cursor.setPosition(0, QTextCursor.KeepAnchor)
 
270
        source = tokeniter.Source.selection(cursor, True)
 
271
    else:
 
272
        source = tokeniter.Source.document(cursor, True)
 
273
    
 
274
    pitches = PitchIterator(source)
 
275
    psource = pitches.pitches()
 
276
    if selection:
 
277
        # consume tokens before the selection, following the language
 
278
        t = source.consume(pitches.tokens(), start)
 
279
        if t:
 
280
            psource = itertools.chain((t,), psource)
 
281
    
 
282
    # this class dispatches the tokens. we can't use a generator function
 
283
    # as that doesn't like to be called again while there is already a body
 
284
    # running.
 
285
    class gen(object):
 
286
        def __iter__(self):
 
287
            return self
 
288
        
 
289
        def __next__(self):
 
290
            t = next(psource)
 
291
            while isinstance(t, (ly.lex.Space, ly.lex.Comment)):
 
292
                t = next(psource)
 
293
            if t == '\\relative' and isinstance(t, ly.lex.lilypond.Command):
 
294
                relative()
 
295
                t = next(psource)
 
296
            elif isinstance(t, ly.lex.lilypond.ChordMode):
 
297
                consume() # do not change chords
 
298
                t = next(psource)
 
299
            elif isinstance(t, ly.lex.lilypond.MarkupScore):
 
300
                consume()
 
301
                t = next(psource)
 
302
            return t
 
303
        
 
304
        next = __next__
 
305
            
 
306
    tsource = gen()
 
307
 
 
308
    def context():
 
309
        """Consume tokens till the level drops (we exit a construct)."""
 
310
        depth = source.state.depth()
 
311
        for t in tsource:
 
312
            yield t
 
313
            if source.state.depth() < depth:
 
314
                return
 
315
    
 
316
    def consume():
 
317
        """Consume tokens from context() returning the last token, if any."""
 
318
        t = None
 
319
        for t in context():
 
320
            pass
 
321
        return t
 
322
    
 
323
    def relative():
 
324
        """Consume the whole \relative expression without doing anything. """
 
325
        # skip pitch argument
 
326
        t = next(tsource)
 
327
        if isinstance(t, Pitch):
 
328
            t = next(tsource)
 
329
        
 
330
        while True:
 
331
            # eat stuff like \new Staff == "bla" \new Voice \notes etc.
 
332
            if isinstance(source.state.parser(), ly.lex.lilypond.ParseNewContext):
 
333
                t = consume()
 
334
            elif isinstance(t, ly.lex.lilypond.NoteMode):
 
335
                t = next(tsource)
 
336
            else:
 
337
                break
 
338
        
 
339
        if t in ('{', '<<', '<'):
 
340
            consume()
 
341
    
 
342
    # Do it!
 
343
    with util.busyCursor():
 
344
        with cursortools.Editor() as editor:
 
345
            for t in tsource:
 
346
                if t in ('{', '<<'):
 
347
                    # Ok, parse current expression.
 
348
                    c = source.cursor(t, end=0) # insert the \relative command
 
349
                    lastPitch = None
 
350
                    chord = None
 
351
                    for t in context():
 
352
                        # skip commands with pitches that do not count
 
353
                        if isinstance(t, ly.lex.lilypond.PitchCommand):
 
354
                            consume()
 
355
                        elif isinstance(t, ly.lex.lilypond.ChordStart):
 
356
                            # Handle chord
 
357
                            chord = []
 
358
                        elif isinstance(t, ly.lex.lilypond.ChordEnd):
 
359
                            if chord:
 
360
                                lastPitch = chord[0]
 
361
                            chord = None
 
362
                        elif isinstance(t, Pitch):
 
363
                            # Handle pitch
 
364
                            if lastPitch is None:
 
365
                                lastPitch = Pitch.c1()
 
366
                                lastPitch.octave = t.octave
 
367
                                if t.note > 3:
 
368
                                    lastPitch.octave += 1
 
369
                                editor.insertText(c,
 
370
                                    "\\relative {0} ".format(
 
371
                                        lastPitch.output(pitches.language)))
 
372
                            p = t.copy()
 
373
                            t.makeRelative(lastPitch)
 
374
                            pitches.write(t, editor)
 
375
                            lastPitch = p
 
376
                            # remember the first pitch of a chord
 
377
                            if chord == []:
 
378
                                chord.append(p)
 
379
 
 
380
 
 
381
def transpose(cursor, mainwindow):
 
382
    """Transposes pitches."""
 
383
    language = documentinfo.info(cursor.document()).pitchLanguage() or 'nederlands'
 
384
    
 
385
    def readpitches(text):
 
386
        """Reads pitches from text."""
 
387
        result = []
 
388
        for pitch, octave in re.findall(r"([a-z]+)([,']*)", text):
 
389
            r = ly.pitch.pitchReader(language)(pitch)
 
390
            if r:
 
391
                result.append(ly.pitch.Pitch(*r, octave=ly.pitch.octaveToNum(octave)))
 
392
        return result
 
393
    
 
394
    def validate(text):
 
395
        """Returns whether the text contains exactly two pitches."""
 
396
        return len(readpitches(text)) == 2
 
397
    
 
398
    text = inputdialog.getText(mainwindow, _("Transpose"), _(
 
399
        "Please enter two absolute pitches, separated by a space, "
 
400
        "using the pitch name language \"{language}\"."
 
401
        ).format(language=language), icon = icons.get('tools_transpose'),
 
402
        help = transpose_help, validate = validate)
 
403
    if text == None:
 
404
        return
 
405
    
 
406
    transposer = ly.pitch.Transposer(*readpitches(text))
 
407
    
 
408
    selection = cursor.hasSelection()
 
409
    if selection:
 
410
        start = cursor.selectionStart()
 
411
        cursor.setPosition(cursor.selectionEnd())
 
412
        cursor.setPosition(0, QTextCursor.KeepAnchor)
 
413
        source = tokeniter.Source.selection(cursor, True)
 
414
    else:
 
415
        source = tokeniter.Source.document(cursor, True)
 
416
    
 
417
    pitches = PitchIterator(source)
 
418
    psource = pitches.pitches()
 
419
    
 
420
    class gen(object):
 
421
        def __init__(self):
 
422
            self.inSelection = not selection
 
423
        
 
424
        def __iter__(self):
 
425
            return self
 
426
        
 
427
        def __next__(self):
 
428
            while True:
 
429
                t = next(psource)
 
430
                if isinstance(t, (ly.lex.Space, ly.lex.Comment)):
 
431
                    continue
 
432
                elif not self.inSelection and pitches.position(t) >= start:
 
433
                    self.inSelection = True
 
434
                # Handle stuff that's the same in relative and absolute here
 
435
                if t == "\\relative":
 
436
                    relative()
 
437
                elif isinstance(t, ly.lex.lilypond.MarkupScore):
 
438
                    absolute(context())
 
439
                elif isinstance(t, ly.lex.lilypond.ChordMode):
 
440
                    chordmode()
 
441
                elif isinstance(t, ly.lex.lilypond.PitchCommand):
 
442
                    if t == "\\transposition":
 
443
                        next(psource) # skip pitch
 
444
                    elif t == "\\transpose":
 
445
                        for p in getpitches(context()):
 
446
                            transpose(p)
 
447
                    elif t == "\\key":
 
448
                        for p in getpitches(context()):
 
449
                            transpose(p, 0)
 
450
                    else:
 
451
                        return t
 
452
                else:
 
453
                    return t
 
454
        
 
455
        next = __next__
 
456
    
 
457
    tsource = gen()
 
458
    
 
459
    def context():
 
460
        """Consume tokens till the level drops (we exit a construct)."""
 
461
        depth = source.state.depth()
 
462
        for t in tsource:
 
463
            yield t
 
464
            if source.state.depth() < depth:
 
465
                return
 
466
    
 
467
    def consume():
 
468
        """Consume tokens from context() returning the last token, if any."""
 
469
        t = None
 
470
        for t in context():
 
471
            pass
 
472
        return t
 
473
        
 
474
    def transpose(p, resetOctave = None):
 
475
        """Transpose absolute pitch, using octave if given."""
 
476
        transposer.transpose(p)
 
477
        if resetOctave is not None:
 
478
            p.octave = resetOctave
 
479
        if tsource.inSelection:
 
480
            pitches.write(p, editor)
 
481
 
 
482
    def chordmode():
 
483
        """Called inside \\chordmode or \\chords."""
 
484
        for p in getpitches(context()):
 
485
            transpose(p, 0)
 
486
            
 
487
    def absolute(tokens):
 
488
        """Called when outside a possible \\relative environment."""
 
489
        for p in getpitches(tokens):
 
490
            transpose(p)
 
491
    
 
492
    def relative():
 
493
        """Called when \\relative is encountered."""
 
494
        def transposeRelative(p, lastPitch):
 
495
            """Transposes a relative pitch; returns the pitch in absolute form."""
 
496
            # absolute pitch determined from untransposed pitch of lastPitch
 
497
            p.makeAbsolute(lastPitch)
 
498
            if not tsource.inSelection:
 
499
                return p
 
500
            # we may change this pitch. Make it relative against the
 
501
            # transposed lastPitch.
 
502
            try:
 
503
                last = lastPitch.transposed
 
504
            except AttributeError:
 
505
                last = lastPitch
 
506
            # transpose a copy and store that in the transposed
 
507
            # attribute of lastPitch. Next time that is used for
 
508
            # making the next pitch relative correctly.
 
509
            newLastPitch = p.copy()
 
510
            transposer.transpose(p)
 
511
            newLastPitch.transposed = p.copy()
 
512
            if p.octaveCheck is not None:
 
513
                p.octaveCheck = p.octave
 
514
            p.makeRelative(last)
 
515
            if relPitch:
 
516
                # we are allowed to change the pitch after the
 
517
                # \relative command. lastPitch contains this pitch.
 
518
                lastPitch.octave += p.octave
 
519
                p.octave = 0
 
520
                pitches.write(lastPitch, editor)
 
521
                del relPitch[:]
 
522
            pitches.write(p, editor)
 
523
            return newLastPitch
 
524
 
 
525
        lastPitch = None
 
526
        relPitch = [] # we use a list so it can be changed from inside functions
 
527
        
 
528
        # find the pitch after the \relative command
 
529
        t = next(tsource)
 
530
        if isinstance(t, Pitch):
 
531
            lastPitch = t
 
532
            if tsource.inSelection:
 
533
                relPitch.append(lastPitch)
 
534
            t = next(tsource)
 
535
        else:
 
536
            lastPitch = Pitch.c1()
 
537
        
 
538
        while True:
 
539
            # eat stuff like \new Staff == "bla" \new Voice \notes etc.
 
540
            if isinstance(source.state.parser(), ly.lex.lilypond.ParseNewContext):
 
541
                t = consume()
 
542
            elif isinstance(t, ly.lex.lilypond.NoteMode):
 
543
                t = next(tsource)
 
544
            else:
 
545
                break
 
546
        
 
547
        # now transpose the relative expression
 
548
        if t in ('{', '<<'):
 
549
            # Handle full music expression { ... } or << ... >>
 
550
            for t in context():
 
551
                if t == '\\octaveCheck':
 
552
                    for p in getpitches(context()):
 
553
                        lastPitch = p.copy()
 
554
                        del relPitch[:]
 
555
                        if tsource.inSelection:
 
556
                            transposer.transpose(p)
 
557
                            lastPitch.transposed = p
 
558
                            pitches.write(p, editor)
 
559
                elif isinstance(t, ly.lex.lilypond.ChordStart):
 
560
                    chord = [lastPitch]
 
561
                    for p in getpitches(context()):
 
562
                        chord.append(transposeRelative(p, chord[-1]))
 
563
                    lastPitch = chord[:2][-1] # same or first
 
564
                elif isinstance(t, Pitch):
 
565
                    lastPitch = transposeRelative(t, lastPitch)
 
566
        elif isinstance(t, ly.lex.lilypond.ChordStart):
 
567
            # Handle just one chord
 
568
            for p in getpitches(context()):
 
569
                lastPitch = transposeRelative(p, lastPitch)
 
570
        elif isinstance(t, Pitch):
 
571
            # Handle just one pitch
 
572
            transposeRelative(token, lastPitch)
 
573
 
 
574
    # Do it!
 
575
    try:
 
576
        with util.busyCursor():
 
577
            with cursortools.Editor() as editor:
 
578
                absolute(tsource)
 
579
    except ly.pitch.PitchNameNotAvailable:
 
580
        QMessageBox.critical(mainwindow, app.caption(_("Transpose")), _(
 
581
            "Can't perform the requested transposition.\n\n"
 
582
            "The transposed music would contain quarter-tone alterations "
 
583
            "that are not available in the pitch language \"{language}\"."
 
584
            ).format(language = pitches.language))
 
585
 
 
586
 
 
587
class PitchIterator(object):
 
588
    """Iterate over notes or pitches in a source."""
 
589
    
 
590
    def __init__(self, source):
 
591
        """Initializes us with a tokeniter.Source.
 
592
        
 
593
        The language is set to "nederlands".
 
594
        
 
595
        """
 
596
        self.source = source
 
597
        self.setLanguage("nederlands")
 
598
    
 
599
    def setLanguage(self, lang):
 
600
        """Changes the pitch name language to use.
 
601
        
 
602
        Called internally when \language or \include tokens are encoutered
 
603
        with a valid language name/file.
 
604
        
 
605
        Sets the language attribute to the language name and the read attribute
 
606
        to an instance of ly.pitch.PitchReader.
 
607
        
 
608
        """
 
609
        if lang in ly.pitch.pitchInfo.keys():
 
610
            self.language = lang
 
611
            return True
 
612
    
 
613
    def position(self, t):
 
614
        """Returns the cursor position for the given token or Pitch."""
 
615
        if isinstance(t, Pitch):
 
616
            return t.noteCursor.selectionStart()
 
617
        else:
 
618
            return self.source.position(t)
 
619
    
 
620
    def tokens(self):
 
621
        """Yield just all tokens from the source, following the language."""
 
622
        for t in self.source:
 
623
            yield t
 
624
            if isinstance(t, ly.lex.lilypond.Keyword):
 
625
                if t in ("\\include", "\\language"):
 
626
                    for t in self.source:
 
627
                        if not isinstance(t, ly.lex.Space) and t != '"':
 
628
                            lang = t[:-3] if t.endswith('.ly') else t[:]
 
629
                            if self.setLanguage(lang):
 
630
                                yield LanguageName(lang, t.pos)
 
631
                            break
 
632
                        yield t
 
633
    
 
634
    def pitches(self):
 
635
        """Yields all tokens, but collects Note and Octave tokens.
 
636
        
 
637
        When a Note is encoutered, also reads octave and octave check and then
 
638
        a Pitch is yielded instead of the tokens.
 
639
        
 
640
        """
 
641
        tokens = self.tokens()
 
642
        for t in tokens:
 
643
            while isinstance(t, ly.lex.lilypond.Note):
 
644
                p = self.read(t)
 
645
                if not p:
 
646
                    break
 
647
                p = Pitch(*p)
 
648
                p.origNoteToken = t
 
649
                p.noteCursor = self.source.cursor(t)
 
650
                p.octaveCursor = self.source.cursor(t, start=len(t))
 
651
                t = None # prevent hang in this loop
 
652
                for t in tokens:
 
653
                    if isinstance(t, ly.lex.lilypond.OctaveCheck):
 
654
                        p.octaveCheck = p.origOctaveCheck = ly.pitch.octaveToNum(t)
 
655
                        p.octaveCheckCursor = self.source.cursor(t)
 
656
                        break
 
657
                    elif isinstance(t, ly.lex.lilypond.Octave):
 
658
                        p.octave = p.origOctave = ly.pitch.octaveToNum(t)
 
659
                        p.octaveCursor = self.source.cursor(t)
 
660
                    elif not isinstance(t, (ly.lex.Space, ly.lex.lilypond.Accidental)):
 
661
                        break
 
662
                yield p
 
663
                if t is None:
 
664
                    break
 
665
            else:
 
666
                yield t
 
667
        
 
668
    def read(self, token):
 
669
        """Reads the token and returns (note, alter) or None."""
 
670
        return ly.pitch.pitchReader(self.language)(token)
 
671
    
 
672
    def write(self, pitch, editor, language=None):
 
673
        """Outputs a changed Pitch to the cursortools.Editor."""
 
674
        writer = ly.pitch.pitchWriter(language or self.language)
 
675
        note = writer(pitch.note, pitch.alter)
 
676
        if note != pitch.origNoteToken:
 
677
            editor.insertText(pitch.noteCursor, note)
 
678
        if pitch.octave != pitch.origOctave:
 
679
            editor.insertText(pitch.octaveCursor, ly.pitch.octaveToString(pitch.octave))
 
680
        if pitch.origOctaveCheck is not None:
 
681
            if pitch.octaveCheck is None:
 
682
                editor.removeSelectedText(pitch.octaveCheckCursor)
 
683
            else:
 
684
                octaveCheck = '=' + ly.pitch.octaveToString(pitch.octaveCheck)
 
685
                editor.insertText(pitch.octaveCheckCursor, octaveCheck)
 
686
 
 
687
 
 
688
class LanguageName(ly.lex.Token):
 
689
    pass
 
690
 
 
691
 
 
692
class Pitch(ly.pitch.Pitch):
 
693
    """A Pitch storing cursors for the note name, octave and octaveCheck."""
 
694
    noteCursor = None
 
695
    octaveCheck = None
 
696
    octaveCursor = None
 
697
    octaveCheckCursor = None
 
698
    origNoteToken = None
 
699
    origOctave = 0
 
700
    origOctaveCheck = None
 
701
 
 
702
 
 
703
def getpitches(iterable):
 
704
    """Consumes iterable but only yields Pitch instances."""
 
705
    for p in iterable:
 
706
        if isinstance(p, Pitch):
 
707
            yield p
 
708
 
 
709
 
 
710
class pitch_help(help.page):
 
711
    def title():
 
712
        return _("Pitch manipulation")
 
713
    
 
714
    def body():
 
715
        return _("""\
 
716
<p>
 
717
Frescobaldi offers the following pitch-manipulating functions,
 
718
all in the menu {menu}:
 
719
</p>
 
720
 
 
721
<dl>
 
722
 
 
723
<dt>Pitch language</dt>
 
724
<dd>
 
725
This translates pitch names in the whole document or a selection.
 
726
</dd>
 
727
 
 
728
<dt>Convert relative music to absolute</dt>
 
729
<dd>
 
730
This converts all <code>\\relative</code> music parts to absolute pitch names.
 
731
It removes, but honours, octave checks.
 
732
</dd>
 
733
 
 
734
<dt>Convert absolute music to relative</dt>
 
735
<dd>
 
736
Checks all toplevel music expressions, changing them into
 
737
<code>\\relative</code> mode as soon as the expression contains a pitch.
 
738
If you want to make separate sub-expressions relative, it may be necessary to
 
739
select music from the first expression, leaving out higher-level opening
 
740
braces.
 
741
</dd>
 
742
 
 
743
</dl>
 
744
""").format(menu=help.menu(_("menu title", "Tools"), _("submenu title", "Pitch")))
 
745
 
 
746
    def children():
 
747
        return (transpose_help,)
 
748
 
 
749
 
 
750
class transpose_help(help.page):
 
751
    def title():
 
752
        return _("Transpose")
 
753
    
 
754
    def body():
 
755
        return _("""\
 
756
<p>
 
757
When transposing music, two absolute pitches need to be given to specify
 
758
the distance to transpose over. The pitches may include octave marks.
 
759
The pitches must be entered in the pitch name language used in the document.
 
760
</p>
 
761
 
 
762
<p>
 
763
The music will then be transposed from the first pitch to the second,
 
764
just as the <code>\\transpose</code> LilyPond command would do.
 
765
</p>
 
766
 
 
767
<p>
 
768
E.g. when transposing a minor third upwards, you would enter:<br />
 
769
<code>c es</code>
 
770
</p>
 
771
 
 
772
<p>
 
773
To transpose down a major second, you can enter:<br />
 
774
<code>c bes,</code>
 
775
</p>
 
776
 
 
777
<p>
 
778
or:<br />
 
779
<code>d c</code>
 
780
</p>
 
781
 
 
782
<p>
 
783
It is also possible to use the transpose function to change a piece of music
 
784
from C-sharp to D-flat, or to specify quarter tones if supported in the
 
785
pitch name language that is used.
 
786
</p>
 
787
 
 
788
<p>
 
789
The transpose function can transpose both relative and absolute music,
 
790
correctly handling key signatures, chordmode and octave checks.
 
791
</p>
 
792
""")
 
793
 
 
794