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