1# This file is part of the Frescobaldi project, http://www.frescobaldi.org/
2#
3# Copyright (c) 2008 - 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"""
21Builds the LilyPond score from the settings in the Score Wizard.
22"""
23
24
25import collections
26import fractions
27import re
28
29import ly.dom
30import i18n.mofile
31import lasptyqu
32
33from . import parts
34
35
36class PartNode(object):
37    """Represents an item with sub-items in the parts tree.
38
39    Sub-items of this items are are split out in two lists: the 'parts' and
40    'groups' attributes.
41
42    Parts ('parts' attribute) are vertically stacked (instrumental parts or
43    staff groups). Groups ('groups' attribute) are horizontally added (score,
44    book, bookpart).
45
46    The Part (containing the widgets) is in the 'part' attribute.
47
48    """
49    def __init__(self, item):
50        """item is a PartItem (QTreeWidgetItem)."""
51        self.part = getattr(item, 'part', None)
52        self.groups = []
53        self.parts = []
54        for i in range(item.childCount()):
55            node = PartNode(item.child(i))
56            if isinstance(node.part, parts._base.Group):
57                self.groups.append(node)
58            else:
59                self.parts.append(node)
60
61
62class PartData(object):
63    r"""Represents what a Part wants to add to the LilyPond score.
64
65    A Part may append to the following instance attributes (which are lists):
66
67    includes:           (string) filename to be included
68    codeblocks:         (ly.dom.LyNode) global blocks of code a part depends on
69    assignments:        (ly.dom.Assignment) assignment of an expression to a
70                        variable, most times the music stub for a part
71    nodes:              (ly.dom.LyNode) the nodes a part adds to the parent \score
72    afterblocks:        (ly.dom.LyNode) other blocks, appended ad the end
73
74    The num instance attribute is set to 0 by default but can be increased by
75    the Builder, when there are more parts of the exact same type in the same
76    score.
77
78    This is used by the builder afterwards to adjust identifiers and instrument
79    names to this.
80
81    """
82    def __init__(self, part, parent=None):
83        """part is a parts._base.Part instance, parent may be another PartData."""
84        if parent:
85            parent.children.append(self)
86        self.isChild = bool(parent)
87        self._name = part.__class__.__name__
88        self.children = []
89        self.num = 0
90        self.includes = []
91        self.codeblocks = []
92        self.assignments = []
93        self.nodes = []
94        self.afterblocks = []
95
96    def name(self):
97        """Returns a name for this part data.
98
99        The name consists of the class name of the part with the value of the num
100        attribute appended as a roman number.
101
102        """
103        if self.num:
104            return self._name + ly.util.int2roman(self.num)
105        return self._name
106
107    def assign(self, name=None):
108        """Creates a ly.dom.Assignment.
109
110        name is a string name, if not given the class name is used with the
111        first letter lowered.
112
113        A ly.dom.Reference is used as the name for the Assignment.
114        The assignment is appended to our assignments and returned.
115
116        The Reference is in the name attribute of the assignment.
117
118        """
119        a = ly.dom.Assignment(ly.dom.Reference(name or ly.util.mkid(self.name())))
120        self.assignments.append(a)
121        return a
122
123    def assignMusic(self, name=None, octave=0, transposition=None):
124        """Creates a ly.dom.Assignment with a \\relative music stub."""
125        a = self.assign(name)
126        stub = ly.dom.Relative(a)
127        ly.dom.Pitch(octave, 0, 0, stub)
128        s = ly.dom.Seq(stub)
129        ly.dom.Identifier(self.globalName, s).after = 1
130        if transposition is not None:
131            toct, tnote, talter = transposition
132            ly.dom.Pitch(toct, tnote, fractions.Fraction(talter, 2), ly.dom.Transposition(s))
133        ly.dom.LineComment(_("Music follows here."), s)
134        ly.dom.BlankLine(s)
135        return a
136
137
138class BlockData(object):
139    """Represents the building blocks of a global section of a ly.dom.Document."""
140    def __init__(self):
141        self.assignments = ly.dom.Block()
142        self.scores = ly.dom.Block()
143        self.backmatter = ly.dom.Block()
144
145
146class Builder(object):
147    """Builds the LilyPond score from all user input in the score wizard.
148
149    Reads settings and other input from the dialog on construction.
150    Does not need the dialog after that.
151
152    """
153    def __init__(self, dialog):
154        """Initializes ourselves from all user settings in the dialog."""
155        self._includeFiles = []
156        self.globalUsed = False
157
158        scoreProperties = dialog.settings.widget().scoreProperties
159        generalPreferences = dialog.settings.widget().generalPreferences
160        lilyPondPreferences = dialog.settings.widget().lilyPondPreferences
161        instrumentNames = dialog.settings.widget().instrumentNames
162
163        # attributes the Part and Container types may read and we need later as well
164        self.header = list(dialog.header.widget().headers())
165        self.headerDict = dict(self.header)
166        self.lyVersionString = lilyPondPreferences.version.currentText().strip()
167        self.lyVersion = tuple(map(int, re.findall('\\d+', self.lyVersionString)))
168        self.midi = generalPreferences.midi.isChecked()
169        self.pitchLanguage = dialog.pitchLanguage()
170        self.suppressTagLine = generalPreferences.tagl.isChecked()
171        self.removeBarNumbers = generalPreferences.barnum.isChecked()
172        self.smartNeutralDirection = generalPreferences.neutdir.isChecked()
173        self.showMetronomeMark = generalPreferences.metro.isChecked()
174        self.paperSize = generalPreferences.getPaperSize()
175        self.paperLandscape = generalPreferences.paperOrientation.currentIndex() == 1
176        self.paperRotated = generalPreferences.paperOrientation.currentIndex() == 2
177        self.showInstrumentNames = instrumentNames.isChecked()
178        names = ['long', 'short', None]
179        self.firstInstrumentName = names[instrumentNames.firstSystem.currentIndex()]
180        self.otherInstrumentName = names[instrumentNames.otherSystems.currentIndex()]
181
182        # translator for instrument names
183        self._ = _
184        if instrumentNames.isChecked():
185            lang = instrumentNames.getLanguage()
186            if lang == 'C':
187                self._ = i18n.translator(None)
188            elif lang:
189                mofile = i18n.find(lang)
190                if mofile:
191                    self._ = i18n.translator(i18n.mofile.MoFile(mofile))
192
193        # global score preferences
194        self.scoreProperties = scoreProperties
195        self.globalSection = scoreProperties.globalSection(self)
196
197        # printer that converts the ly.dom tree to text
198        p = self._printer = ly.dom.Printer()
199        p.indentString = "  " # will be re-indented anyway
200        p.typographicalQuotes = generalPreferences.typq.isChecked()
201        quotes = lasptyqu.preferred()
202        p.primary_quote_left = quotes.primary.left
203        p.primary_quote_right = quotes.primary.right
204        p.secondary_quote_left = quotes.secondary.left
205        p.secondary_quote_right = quotes.secondary.right
206        if self.pitchLanguage:
207            p.language = self.pitchLanguage
208
209        # get the parts
210        globalGroup = PartNode(dialog.parts.widget().rootPartItem())
211
212        # move parts down the tree to subgroups that have no parts
213        assignparts(globalGroup)
214
215        # now prepare the different blocks
216        self.usePrefix = needsPrefix(globalGroup)
217        self.currentScore = 0
218
219        # make a part of the document (assignments, scores, backmatter) for
220        # every group (book, bookpart or score) in the global group
221        if globalGroup.parts:
222            groups = [globalGroup]
223        else:
224            groups = globalGroup.groups
225
226        self.blocks = []
227        for group in groups:
228            block = BlockData()
229            self.makeBlock(group, block.scores, block)
230            self.blocks.append(block)
231
232    def makeBlock(self, group, node, block):
233        """Recursively populates the Block with data from the group.
234
235        The group can contain parts and/or subgroups.
236        ly.dom.LyNodes representing the LilyPond document are added to the node.
237
238        """
239        if group.part:
240            node = group.part.makeNode(node)
241        if group.parts:
242            # prefix for this block, used if necessary
243            self.currentScore += 1
244            prefix = 'score' + ly.util.int2letter(self.currentScore)
245
246            # is this a score and has it its own score properties?
247            globalName = 'global'
248            scoreProperties = self.scoreProperties
249            if isinstance(group.part, parts.containers.Score):
250                globalSection = group.part.globalSection(self)
251                if globalSection:
252                    scoreProperties = group.part
253                    globalName = prefix + 'Global'
254                    a = ly.dom.Assignment(globalName, block.assignments)
255                    a.append(globalSection)
256                    ly.dom.BlankLine(block.assignments)
257            if globalName == 'global':
258                self.globalUsed = True
259
260            # add parts here, always in \score { }
261            score = node if isinstance(node,ly.dom.Score) else ly.dom.Score(node)
262            ly.dom.Layout(score)
263            if self.midi:
264                midi = ly.dom.Midi(score)
265                # set MIDI tempo if necessary
266                if not self.showMetronomeMark:
267                    if self.lyVersion >= (2, 16, 0):
268                        scoreProperties.lySimpleMidiTempo(midi)
269                        midi[0].after = 1
270                    else:
271                        scoreProperties.lyMidiTempo(ly.dom.Context('Score', midi))
272            music = ly.dom.Simr()
273            score.insert(0, music)
274
275            # a PartData subclass "knowing" the globalName and scoreProperties
276            class _PartData(PartData): pass
277            _PartData.globalName = globalName
278            _PartData.scoreProperties = scoreProperties
279
280            # make the parts
281            partData = self.makeParts(group.parts, _PartData)
282
283            # record the include files a part wants to add
284            for p in partData:
285                for i in p.includes:
286                    if i not in self._includeFiles:
287                        self._includeFiles.append(i)
288
289            # collect all 'prefixable' assignments for this group
290            assignments = []
291            for p in partData:
292                assignments.extend(p.assignments)
293
294            # add the assignments to the block
295            for p in partData:
296                for a in p.assignments:
297                    block.assignments.append(a)
298                    ly.dom.BlankLine(block.assignments)
299                block.backmatter.extend(p.afterblocks)
300
301            # make part assignments if there is more than one part that has assignments
302            if sum(1 for p in partData if p.assignments) > 1:
303                def make(part, music):
304                    if part.assignments:
305                        a = ly.dom.Assignment(ly.dom.Reference(ly.util.mkid(part.name() + "Part")))
306                        ly.dom.Simr(a).extend(part.nodes)
307                        ly.dom.Identifier(a.name, music).after = 1
308                        block.assignments.append(a)
309                        ly.dom.BlankLine(block.assignments)
310                        assignments.append(a)
311                    else:
312                        music.extend(part.nodes)
313            else:
314                def make(part, music):
315                    music.extend(part.nodes)
316
317            def makeRecursive(parts, music):
318                for part in parts:
319                    make(part, music)
320                    if part.children:
321                        makeRecursive(part.children, part.music)
322
323            parents = [p for p in partData if not p.isChild]
324            makeRecursive(parents, music)
325
326            # add the prefix to the assignments if necessary
327            if self.usePrefix:
328                for a in assignments:
329                    a.name.name = ly.util.mkid(prefix, a.name.name)
330
331        for g in group.groups:
332            self.makeBlock(g, node, block)
333
334    def makeParts(self, parts, partDataClass):
335        """Lets the parts build the music stubs and assignments.
336
337        parts is a list of PartNode instances.
338        partDataClass is a subclass or PartData containing some attributes:
339            - globalName is either 'global' (for the global time/key signature
340              section) or something like 'scoreAGlobal' (when a score has its
341              own properties).
342            - scoreProperties is the ScoreProperties instance currently in effect
343              (the global one or a particular Score part's one).
344
345        Returns the list of PartData object for the parts.
346
347        """
348        # number instances of the same type (Choir I and Choir II, etc.)
349        data = {}
350        types = collections.defaultdict(list)
351        def _search(parts, parent=None):
352            for group in parts:
353                pd = data[group] = partDataClass(group.part, parent)
354                types[pd.name()].append(group)
355                _search(group.parts, pd)
356        _search(parts)
357        for t in types.values():
358            if len(t) > 1:
359                for num, group in enumerate(t, 1):
360                    data[group].num = num
361
362        # now build all the parts
363        for group in allparts(parts):
364            group.part.build(data[group], self)
365
366        # check for name collisions in assignment identifiers
367        # add the part class name and a roman number if necessary
368        refs = collections.defaultdict(list)
369        for group in allparts(parts):
370            for a in data[group].assignments:
371                ref = a.name
372                name = ref.name
373                refs[name].append((ref, group))
374        for reflist in refs.values():
375            if len(reflist) > 1:
376                for ref, group in reflist:
377                    # append the class name and number
378                    ref.name = ly.util.mkid(ref.name, data[group].name())
379
380        # return all PartData instances
381        return [data[group] for group in allparts(parts)]
382
383    def text(self, doc=None):
384        """Return LilyPond formatted output. """
385        return self.printer().indent(doc or self.document())
386
387    def printer(self):
388        """Returns a ly.dom.Printer, that converts the ly.dom structure to LilyPond text. """
389        return self._printer
390
391    def document(self):
392        """Creates and returns a ly.dom tree representing the full LilyPond document."""
393        doc = ly.dom.Document()
394
395        # version
396        ly.dom.Version(self.lyVersionString, doc)
397
398        # language
399        if self.pitchLanguage:
400            if self.lyVersion >= (2, 13, 38):
401                ly.dom.Line('\\language "{0}"'.format(self.pitchLanguage), doc)
402            else:
403                ly.dom.Include("{0}.ly".format(self.pitchLanguage), doc)
404        ly.dom.BlankLine(doc)
405
406        # other include files
407        if self._includeFiles:
408            for filename in self._includeFiles:
409                ly.dom.Include(filename, doc)
410            ly.dom.BlankLine(doc)
411
412        # general header
413        h = ly.dom.Header()
414        for name, value in self.header:
415            h[name] = value
416        if 'tagline' not in h and self.suppressTagLine:
417            ly.dom.Comment(_("Remove default LilyPond tagline"), h)
418            h['tagline'] = ly.dom.Scheme('#f')
419        if len(h):
420            doc.append(h)
421            ly.dom.BlankLine(doc)
422
423        # paper size
424        if self.paperSize:
425            ly.dom.Scheme(
426                '(set-paper-size "{0}{1}"{2})'.format(
427                    self.paperSize,
428                    "landscape" if self.paperLandscape else "",
429                " 'landscape" if self.paperRotated else ""),
430                ly.dom.Paper(doc)
431            ).after = 1
432            ly.dom.BlankLine(doc)
433
434        layout = ly.dom.Layout()
435
436        # remove bar numbers
437        if self.removeBarNumbers:
438            ly.dom.Line('\\remove "Bar_number_engraver"',
439                ly.dom.Context('Score', layout))
440
441        # smart neutral direction
442        if self.smartNeutralDirection:
443            ctxt_voice = ly.dom.Context('Voice', layout)
444            ly.dom.Line('\\consists "Melody_engraver"', ctxt_voice)
445            ly.dom.Line("\\override Stem #'neutral-direction = #'()", ctxt_voice)
446
447        if len(layout):
448            doc.append(layout)
449            ly.dom.BlankLine(doc)
450
451        # global section
452        if self.globalUsed:
453            a = ly.dom.Assignment('global')
454            a.append(self.globalSection)
455            doc.append(a)
456            ly.dom.BlankLine(doc)
457
458        # add the main scores
459        for block in self.blocks:
460            doc.append(block.assignments)
461            doc.append(block.scores)
462            ly.dom.BlankLine(doc)
463            if len(block.backmatter):
464                doc.append(block.backmatter)
465                ly.dom.BlankLine(doc)
466        return doc
467
468    def setMidiInstrument(self, node, midiInstrument):
469        """Sets the MIDI instrument for the node, if the user wants MIDI output."""
470        if self.midi:
471            node.getWith()['midiInstrument'] = midiInstrument
472
473    def setInstrumentNames(self, staff, longName, shortName):
474        """Sets the instrument names to the staff (or group).
475
476        longName and shortName may either be a string or a ly.dom.LyNode object (markup)
477        The settings in the score wizard are honored.
478
479        """
480        if self.showInstrumentNames:
481            staff.addInstrumentNameEngraverIfNecessary()
482            w = staff.getWith()
483            first = None
484            if self.firstInstrumentName:
485                first = longName if self.firstInstrumentName == 'long' else shortName
486                w['instrumentName'] = first
487            if self.otherInstrumentName:
488                other = longName if self.otherInstrumentName == 'long' else shortName
489                # If these are markup objects, copy them otherwise the assignment
490                # to shortInstrumentName takes it away from the instrumentName.
491                if other is first and isinstance(first, ly.dom.LyNode):
492                    other = other.copy()
493                w['shortInstrumentName'] = other
494
495    def instrumentName(self, function, num=0):
496        """Returns an instrument name.
497
498        The name is constructed by calling the 'function' with our translator as
499        argument, and appending the number 'num' in roman literals, if num > 0.
500
501        """
502        name = function(self._)
503        if num:
504            name += ' ' + ly.util.int2roman(num)
505        return name
506
507    def setInstrumentNamesFromPart(self, node, part, data):
508        """Sets the long and short instrument names for the node.
509
510        Calls part.title(translator) and part.short(translator) to get the
511        names, appends roman literals if data.num > 0, and sets them on the node.
512
513        """
514        longName = self.instrumentName(part.title, data.num)
515        shortName = self.instrumentName(part.short, data.num)
516        self.setInstrumentNames(node, longName, shortName)
517
518
519def assignparts(group):
520    """Moves the parts to sub-groups that contain no parts.
521
522    If at least one subgroup uses the parts, the parent's parts are removed.
523    This way a user can specify some parts and then multiple scores, and they will all
524    use the same parts again.
525
526    """
527    partsOfParentUsed = False
528    for g in group.groups:
529        if not g.parts:
530            g.parts = group.parts
531            partsOfParentUsed = True
532        assignparts(g)
533    if partsOfParentUsed:
534        group.parts = []
535
536
537def itergroups(group):
538    """Iterates over the group and its subgroups as an event list.
539
540    When a group is yielded, it means the group starts.
541    When None is yielded, it means that the last started groups ends.
542
543    """
544    yield group
545    for g in group.groups:
546        for i in itergroups(g):
547            yield i
548    yield None # end a group
549
550
551def descendants(group):
552    """Iterates over the descendants of a group (including the group itself).
553
554    First the group, then its children, then the grandchildren, etc.
555
556    """
557    def _descendants(group):
558        children = group.groups
559        while children:
560            new = []
561            for g in children:
562                yield g
563                new.extend(g.groups)
564            children = new
565    yield group
566    for g in _descendants(group):
567        yield g
568
569
570def needsPrefix(globalGroup):
571    """Returns True if there are multiple scores in group with shared part types.
572
573    This means the music assignments will need a prefix (e.g. scoreAsoprano,
574    scoreBsoprano, etc.)
575
576    """
577    counter = collections.Counter()
578    for group in itergroups(globalGroup):
579        if group:
580            counter.update(type(g.part) for g in group.parts)
581    return bool(counter) and max(counter.values()) > 1
582
583
584def allparts(parts):
585    """Yields all the parts and child parts."""
586    for group in parts:
587        yield group
588        for group in allparts(group.parts):
589            yield group
590
591