def defaultJob(document, preview): """Returns a default job for the document.""" filename, mode, includepath = documentinfo.info(document).jobinfo(True) includepath.extend(documentinfo.info(document).includepath()) i = info(document) j = job.Job() command = [i.command] s = QSettings() s.beginGroup("lilypond_settings") if s.value("delete_intermediate_files", True) not in (False, "false"): command.append('-ddelete-intermediate-files') else: command.append('-dno-delete-intermediate-files') command.append('-dpoint-and-click' if preview else '-dno-point-and-click') command.append('--pdf') command.extend('-I' + path for path in includepath) j.directory = os.path.dirname(filename) command.append(filename) j.command = command if s.value("no_translation", False) in (True, "true"): j.environment['LANG'] = 'C' j.setTitle("{0} {1} [{2}]".format( os.path.basename(i.command), i.versionString(), document.documentName())) return j
def getTransposer(document, mainwindow): """Show a dialog and return the desired transposer. Returns None if the dialog was cancelled. """ language = documentinfo.info(document).pitchLanguage() or 'nederlands' def readpitches(text): """Reads pitches from text.""" result = [] for pitch, octave in re.findall(r"([a-z]+)([,']*)", text): r = ly.pitch.pitchReader(language)(pitch) if r: result.append(ly.pitch.Pitch(*r, octave=ly.pitch.octaveToNum(octave))) return result def validate(text): """Returns whether the text contains exactly two pitches.""" return len(readpitches(text)) == 2 text = inputdialog.getText(mainwindow, _("Transpose"), _( "Please enter two absolute pitches, separated by a space, " "using the pitch name language \"{language}\"." ).format(language=language), icon = icons.get('tools-transpose'), help = transpose_help, validate = validate) if text: return ly.pitch.Transposer(*readpitches(text))
def include_markup_commands(cursor): """Harvest markup command definitions from included files.""" includeargs = ly.parse.includeargs(itertools.chain.from_iterable(tokens(cursor))) dinfo = documentinfo.info(cursor.document()) fname = cursor.document().url().toLocalFile() files = fileinfo.includefiles(fname, dinfo.includepath(), includeargs) return itertools.chain.from_iterable(fileinfo.FileInfo.info(f).markup_commands() for f in files)
def open_file_at_cursor(cursor, mainwin): """Opens the filename mentioned at the text cursor.""" # take either the selection or the include-args found by ly.parse if cursor.hasSelection(): fnames = [cursor.selection().toPlainText()] else: fnames = list(ly.parse.includeargs(iter(tokeniter.tokens(cursor.block())))) # detemine search path: doc dir and other include path names filename = cursor.document().url().toLocalFile() if filename: path = [os.path.dirname(filename)] else: path = [] path.extend(documentinfo.info(cursor.document()).includepath()) # load all docs, trying all include paths d = None for f in fnames: for p in path: name = os.path.normpath(os.path.join(p, f)) if os.access(name, os.R_OK): d = mainwin.openUrl(QUrl.fromLocalFile(name)) break if d: mainwin.setCurrentDocument(d, True)
def setLanguageMenu(self): """Called when the menu is shown; selects the correct language.""" import documentinfo doc = self.mainwindow().currentDocument() lang = documentinfo.info(doc).pitchLanguage() or 'nederlands' for a in self.language_group.actions(): if a.objectName() == lang: a.setChecked(True) break
def setDocument(self, doc): v = documentinfo.info(doc).versionString() if v: self.fromVersion.setText(v) self.reason.setText(_("(set in document)")) else: self.reason.clear() self._text = doc.toPlainText() self._encoding = doc.encoding() or "UTF-8" self.setConvertedText()
def defaultJob(document, args=None): """Return a default job for the document. The 'args' argument, if given, must be a list of commandline arguments that are given to LilyPond, and may enable specific preview modes. If args is not given, the Job will cause LilyPond to run in Publish mode, with point and click turned off. """ filename, includepath = documentinfo.info(document).jobinfo(True) i = info(document) j = job.Job() command = [i.abscommand() or i.command] s = QSettings() s.beginGroup("lilypond_settings") if s.value("delete_intermediate_files", True, bool): command.append('-ddelete-intermediate-files') else: command.append('-dno-delete-intermediate-files') if args: command.extend(args) else: # publish mode command.append('-dno-point-and-click') if s.value("default_output_target", "pdf", str) == "svg": # engrave to SVG command.append('-dbackend=svg') else: # engrave to PDF if not args: # publish mode if s.value("embed_source_code", False, bool) and i.version() >= (2, 19, 39): command.append('-dembed-source-code') command.append('--pdf') command.extend('-I' + path for path in includepath) j.directory = os.path.dirname(filename) command.append(filename) j.command = command j.environment['LD_LIBRARY_PATH'] = i.libdir() if s.value("no_translation", False, bool): j.environment['LANG'] = 'C' j.environment['LC_ALL'] = 'C' j.set_title("{0} {1} [{2}]".format(os.path.basename(i.command), i.versionString(), document.documentName())) return j
def defaultJob(document, args=None): """Return a default job for the document. The 'args' argument, if given, must be a list of commandline arguments that are given to LilyPond, and may enable specific preview modes. If args is not given, the Job will cause LilyPond to run in Publish mode, with point and click turned off. """ filename, includepath = documentinfo.info(document).jobinfo(True) i = info(document) j = job.Job() command = [i.abscommand() or i.command] s = QSettings() s.beginGroup("lilypond_settings") if s.value("delete_intermediate_files", True, bool): command.append('-ddelete-intermediate-files') else: command.append('-dno-delete-intermediate-files') if args: command.extend(args) else: # publish mode command.append('-dno-point-and-click') if s.value("default_output_target", "pdf", str) == "svg": # engrave to SVG command.append('-dbackend=svg') else: # engrave to PDF if not args: # publish mode if s.value("embed_source_code", False, bool) and i.version() >= (2, 19, 39): command.append('-dembed-source-code') command.append('--pdf') command.extend('-I' + path for path in includepath) j.directory = os.path.dirname(filename) command.append(filename) j.command = command j.environment['LD_LIBRARY_PATH'] = i.libdir() if s.value("no_translation", False, bool): j.environment['LANG'] = 'C' j.environment['LC_ALL'] = 'C' j.set_title("{0} {1} [{2}]".format( os.path.basename(i.command), i.versionString(), document.documentName())) return j
def currentDirectory(self): """Returns the directory the document resides in. Returns the temporary directory if that was used last and there is no master document set. """ if documentinfo.info(self.document()).master(): filename = self.document().url().toLocalFile() else: filename = self.jobfile() directory = os.path.dirname(filename) if os.path.isdir(directory): return directory
def getJob(self, document): """Returns a Job to start.""" filename, mode, includepath = documentinfo.info(document).jobinfo(True) includepath.extend(documentinfo.info(document).includepath()) i = self._infos[self.versionCombo.currentIndex()] cmd = [] for t in self.commandLine.toPlainText().split(): if t == '$lilypond': cmd.append(i.command) elif t == '$filename': cmd.append(filename) elif t == '$include': cmd.extend('-I' + path for path in includepath) else: cmd.append(t) j = job.Job() j.directory = os.path.dirname(filename) j.command = cmd if self.englishCheck.isChecked(): j.environment['LANG'] = 'C' j.setTitle("{0} {1} [{2}]".format( os.path.basename(i.command), i.versionString(), document.documentName())) return j
def includenames(self, cursor, directory=None): """Finds files relative to the directory of the cursor's document. If the document has a local filename, looks in that directory, also in a subdirectory of it, if the directory argument is given. Then looks recursively in the user-set include paths, and finally in LilyPond's own ly/ folder. """ names = [] # names in current dir path = self.document().url().toLocalFile() if path: basedir = os.path.dirname(path) if directory: basedir = os.path.join(basedir, directory) names.extend( sorted( os.path.join(directory, f) for f in get_filenames(basedir, True))) else: names.extend(sorted(get_filenames(basedir, True))) # names in specified include paths import documentinfo for basedir in documentinfo.info(self.document()).includepath(): # store dir relative to specified include path root reldir = directory if directory else "" # look for files in the current relative directory for f in sorted(get_filenames(os.path.join(basedir, reldir), True)): names.append(os.path.join(reldir, f)) # names from LilyPond itself import engrave.command datadir = engrave.command.info(self.document()).datadir() if datadir: basedir = os.path.join(datadir, 'ly') # get the filenames but avoid the -init files here names.extend( sorted(f for f in get_filenames(basedir) if not f.endswith('init.ly') and f.islower())) # forward slashes on Windows (issue #804) if os.name == "nt": names = [name.replace('\\', '/') for name in names] return listmodel.ListModel(names)
def saveDocumentInfo(self): """Takes over some vital information from a DocumentInfo instance. The file a job is run on and the basenames expected to be created are saved. When the user saves a Document after a Job has run, this information is 'forgotten' again. Otherwise the results of a Job would not be seen if the user starts a Job and then saves the Document while the job is still running. The Job uses the scratcharea if the document was modified but saving it would result in DocumentInfo.jobinfo()[0] pointing to the real document instead. """ info = documentinfo.info(self.document()) self._jobfile = info.jobinfo()[0] self._basenames = info.basenames()
def include_identifiers(cursor): """Harvests identifier definitions from included files.""" def tokens(): end = cursor.block() block = cursor.document().firstBlock() while block < end: yield tokeniter.tokens(block) block = block.next() includeargs = ly.parse.includeargs(itertools.chain.from_iterable(tokens())) dinfo = documentinfo.info(cursor.document()) fname = cursor.document().url().toLocalFile() files = fileinfo.includefiles(fname, dinfo.includepath(), includeargs) return itertools.chain.from_iterable(fileinfo.FileInfo.info(f).names() for f in files)
def includenames(self, cursor, directory=None): """Finds files relative to the directory of the cursor's document. If the document has a local filename, looks in that directory, also in a subdirectory of it, if the directory argument is given. Then looks recursively in the user-set include paths, and finally in LilyPond's own ly/ folder. """ names = [] # names in current dir path = self.document().url().toLocalFile() if path: basedir = os.path.dirname(path) if directory: basedir = os.path.join(basedir, directory) names.extend(sorted(os.path.join(directory, f) for f in get_filenames(basedir, True))) else: names.extend(sorted(get_filenames(basedir, True))) # names in specified include paths import documentinfo for basedir in documentinfo.info(self.document()).includepath(): # store dir relative to specified include path root reldir = directory if directory else "" # look for files in the current relative directory for f in sorted(get_filenames(os.path.join(basedir, reldir), True)): names.append(os.path.join(reldir, f)) # names from LilyPond itself import engrave.command datadir = engrave.command.info(self.document()).datadir() if datadir: basedir = os.path.join(datadir, 'ly') # get the filenames but avoid the -init files here names.extend(sorted(f for f in get_filenames(basedir) if not f.endswith('init.ly') and f.islower())) # forward slashes on Windows (issue #804) if os.name == "nt": names = [name.replace('\\', '/') for name in names] return listmodel.ListModel(names)
def defaultJob(document, args=None): """Return a default job for the document. The 'args' argument, if given, must be a list of commandline arguments that are given to LilyPond, and may enable specific preview modes. If args is not given, the Job will cause LilyPond to run in Publish mode, with point and click turned off. """ filename, includepath = documentinfo.info(document).jobinfo(True) i = info(document) j = job.Job() command = [i.abscommand() or i.command] s = QSettings() s.beginGroup("lilypond_settings") if s.value("delete_intermediate_files", True, bool): command.append("-ddelete-intermediate-files") else: command.append("-dno-delete-intermediate-files") if args: command.extend(args) else: command.append("-dno-point-and-click") if s.value("default_output_target", "pdf", type("")) == "svg": command.append("-dbackend=svg") else: command.append("--pdf") command.extend("-I" + path for path in includepath) j.directory = os.path.dirname(filename) command.append(filename) j.command = command if s.value("no_translation", False, bool): j.environment["LANG"] = "C" j.setTitle("{0} {1} [{2}]".format(os.path.basename(i.command), i.versionString(), document.documentName())) return j
def filenames_at_cursor(cursor, existing=True): """Return a list of filenames at the cursor. If existing is False, also names are returned that do not exist on disk. """ # take either the selection or the include-args found by lydocinfo start = cursor.document().findBlock(cursor.selectionStart()).position() end = cursor.selectionEnd() if not cursor.hasSelection(): end = start + len(cursor.block().text()) + 1 dinfo = documentinfo.info(cursor.document()) i = dinfo.lydocinfo().range(start, end) fnames = i.include_args() or i.scheme_load_args() if not fnames and cursor.hasSelection(): text = cursor.selection().toPlainText() if '\n' not in text.strip(): fnames = [text] # determine search path: doc dir and other include path names filename = cursor.document().url().toLocalFile() directory = os.path.dirname(filename) if filename: path = [directory] else: path = [] path.extend(dinfo.includepath()) # find all docs, trying all include paths filenames = [] for f in fnames: for p in path: name = os.path.normpath(os.path.join(p, f)) if os.access(name, os.R_OK): filenames.append(name) break else: if not existing: name = os.path.normpath(os.path.join(directory, f)) filenames.append(name) return filenames
def tree(urls=False): """Return the open documents as a tree structure. Returned is a ly.node.Node instance having the toplevel documents (documents that are not included by other open documents) as children. The children of the nodes are the documents that are included by the toplevel document. Every node has the Document in its document attribute. If urls == True, nodes will also be generated for urls that refer to documents that are not yet open. They will have the QUrl in their url attribute. It is not checked whether the referred to urls or files actually exist. """ root = ly.node.Node() nodes = {} for doc in app.documents: try: n = nodes[doc] except KeyError: n = nodes[doc] = DocumentNode(root) n.document = doc for u in documentinfo.info(doc).child_urls(): d = app.findDocument(u) if d: try: n.append(nodes[d]) except KeyError: n1 = nodes[d] = DocumentNode(n) n1.document = d elif urls: try: n.append(nodes[u.toString()]) except KeyError: n1 = nodes[u.toString()] = DocumentNode(n) n1.url = u return root
def getJob(self, document): """Returns a Job to start.""" filename, includepath = documentinfo.info(document).jobinfo(True) i = self.lilyChooser.lilyPondInfo() cmd = [] for t in self.commandLine.toPlainText().split(): if t == "$lilypond": cmd.append(i.abscommand() or i.command) elif t == "$filename": cmd.append(filename) elif t == "$include": cmd.extend("-I" + path for path in includepath) else: cmd.append(t) j = job.Job() j.directory = os.path.dirname(filename) j.command = cmd if self.englishCheck.isChecked(): j.environment["LANG"] = "C" j.environment["LC_ALL"] = "C" j.set_title("{0} {1} [{2}]".format(os.path.basename(i.command), i.versionString(), document.documentName())) return j
def __init__(self, doc, args=None, title=""): """Create a LilyPond job by first retrieving some context from the document and feeding this into job.Job's __init__().""" if isinstance(doc, QUrl): doc = document.Document(doc) self.document = doc self.document_info = docinfo = documentinfo.info(doc) self.lilypond_info = docinfo.lilypondinfo() self._d_options = {} self._backend_args = [] input, self.includepath = docinfo.jobinfo(True) directory = os.path.dirname(input) super(LilyPondJob, self).__init__( encoding='utf-8', args=args, input=input, decode_errors='replace', directory=directory, environment={ 'LD_LIBRARY_PATH': self.lilypond_info.libdir() }, title=title, priority=2) # Set default values from Preferences s = QSettings() s.beginGroup("lilypond_settings") self.set_d_option('delete-intermediate-files', s.value("delete_intermediate_files", True, bool)) self.default_output_target = s.value( "default_output_target", "pdf", str) self.embed_source_code = s.value("embed_source_code", False, bool) if s.value("no_translation", False, bool): self.environment['LANG'] = 'C' self.environment['LC_ALL'] = 'C' self.set_title("{0} {1} [{2}]".format( os.path.basename(self.lilypond_info.command), self.lilypond_info.versionString(), doc.documentName()))
def includeTarget(cursor): """Given a cursor determine an absolute path to an include file present below the cursor. Return path or empty string if no valid file is found. Note that there is still functionality related to opening all targets in the current block. Once it has decided that we only want to open *one* target at a time we should change from a list back to a single string (here and in view.py). """ block = cursor.block() cursor_pos = cursor.position() - block.position() fnames = [] m = incl_regex.search(block.text()) while m: start = m.span()[0] + len(m.group(1)) if start <= cursor_pos <= m.span()[1] - 1: fnames.append(m.group(2)) break m = incl_regex.search(block.text(), m.span()[1]) if not fnames: return "" # determine search path: doc dir and other include path names filename = cursor.document().url().toLocalFile() path = [os.path.dirname(filename)] if filename else [] dinfo = documentinfo.info(cursor.document()) path.extend(dinfo.includepath()) targets = [] # iterating over the search paths, find the first combination pointing to an existing file for f in fnames: for p in path: name = os.path.normpath(os.path.join(p, f)) if os.path.exists(name) and not os.path.isdir(name): targets.append(name) continue return targets
def includenames(self, cursor, directory=None): """Finds files relative to the directory of the cursor's document. If the document has a local filename, looks in that directory, also in a subdirectory of it, if the directory argument is given. Then looks in the user-set include paths, and finally in LilyPond's own ly/ folder. """ names = [] # names in current dir path = self.document().url().toLocalFile() if path: basedir = os.path.dirname(path) if directory: basedir = os.path.join(basedir, directory) names.extend(sorted(os.path.join(directory, f) for f in get_filenames(basedir, True))) else: names.extend(sorted(get_filenames(basedir, True))) # names in specified include paths import documentinfo for basedir in documentinfo.info(self.document()).includepath(): names.extend(sorted(get_filenames(basedir))) # names from LilyPond itself import engrave.command datadir = engrave.command.info(self.document()).datadir() if datadir: basedir = os.path.join(datadir, 'ly') # get the filenames but avoid the -init files here names.extend(sorted(f for f in get_filenames(basedir) if not f.endswith('init.ly') and f.islower())) return listmodel.ListModel(names)
def insertLanguage(document, language): """Inserts a language command in the document. The command is inserted at the top or just below the version line. If the document uses LilyPond < 2.13.38, the \\include command is used, otherwise the newer \\language command. """ version = (documentinfo.info(document).version() or lilypondinfo.preferred().version()) if version and version < (2, 13, 38): text = '\\include "{0}.ly"' else: text = '\\language "{0}"' # insert language command on top of file, but below version block = document.firstBlock() c = QTextCursor(block) if '\\version' in tokeniter.tokens(block): c.movePosition(QTextCursor.EndOfBlock) text = '\n' + text else: text += '\n' c.insertText(text.format(language))
def getJob(self, document): """Returns a Job to start.""" filename, includepath = documentinfo.info(document).jobinfo(True) i = self.lilyChooser.lilyPondInfo() cmd = [] for t in self.commandLine.toPlainText().split(): if t == '$lilypond': cmd.append(i.abscommand() or i.command) elif t == '$filename': cmd.append(filename) elif t == '$include': cmd.extend('-I' + path for path in includepath) else: cmd.append(t) j = job.Job() j.directory = os.path.dirname(filename) j.command = cmd if self.englishCheck.isChecked(): j.environment['LANG'] = 'C' j.setTitle("{0} {1} [{2}]".format(os.path.basename(i.command), i.versionString(), document.documentName())) return j
def getModalTransposer(document, mainwindow): """Show a dialog and return the desired modal transposer. Returns None if the dialog was cancelled. """ language = documentinfo.info(document).pitchLanguage() or 'nederlands' def readpitches(text): """Reads pitches from text.""" result = [] for pitch, octave in re.findall(r"([a-z]+)([,']*)", text): r = ly.pitch.pitchReader(language)(pitch) if r: result.append(ly.pitch.Pitch(*r, octave=ly.pitch.octaveToNum(octave))) return result def validate(text): """Returns whether the text is an integer followed by the name of a key.""" words = text.split() if len(words) != 2: return False try: steps = int(words[0]) keyIndex = ly.pitch.ModalTransposer.getKeyIndex(words[1]) return True except ValueError: return False text = inputdialog.getText(mainwindow, _("Transpose"), _( "Please enter the number of steps to alter by, followed by a key signature. (i.e. \"5 F\")" ), icon = icons.get('tools-transpose'), help = transpose_help, validate = validate) if text: words = text.split() return ly.pitch.ModalTransposer(int(words[0]), ly.pitch.ModalTransposer.getKeyIndex(words[1]))
def jobfile(self): """Returns the file that is currently being, or will be, engraved.""" if self._jobfile is None: return documentinfo.info(self.document()).jobinfo()[0] return self._jobfile
def include_identifiers(cursor): """Harvests identifier definitions from included files.""" dinfo = documentinfo.info(cursor.document()) files = fileinfo.includefiles(get_docinfo(cursor), dinfo.includepath()) return itertools.chain.from_iterable( fileinfo.docinfo(f).definitions() for f in files)
def include_markup_commands(cursor): """Harvest markup command definitions from included files.""" dinfo = documentinfo.info(cursor.document()) files = fileinfo.includefiles(get_docinfo(cursor), dinfo.includepath()) return itertools.chain.from_iterable( fileinfo.docinfo(f).markup_definitions() for f in files)
def get_docinfo(cursor): """Return a ly DocInfo instance for the cursor's document up to its position.""" dinfo = documentinfo.info(cursor.document()) return dinfo.lydocinfo().range(0, cursor.position())
def basenames(self): """Returns the list of basenames the last or running Job is expected to create.""" if self._basenames is None: return documentinfo.info(self.document()).basenames() return self._basenames
def include_identifiers(cursor): """Harvests identifier definitions from included files.""" dinfo = documentinfo.info(cursor.document()) files = fileinfo.includefiles(get_docinfo(cursor), dinfo.includepath()) return itertools.chain.from_iterable(fileinfo.docinfo(f).definitions() for f in files)
def include_markup_commands(cursor): """Harvest markup command definitions from included files.""" dinfo = documentinfo.info(cursor.document()) files = fileinfo.includefiles(get_docinfo(cursor), dinfo.includepath()) return itertools.chain.from_iterable(fileinfo.docinfo(f).markup_definitions() for f in files)
def info(document): """Returns a LilyPondInfo instance that should be used by default to engrave the document.""" version = documentinfo.info(document).version() if version and QSettings().value("lilypond_settings/autoversion", False) in (True, "true"): return lilypondinfo.suitable(version) return lilypondinfo.preferred()
def transpose(cursor, mainwindow): """Transposes pitches.""" language = documentinfo.info(cursor.document()).pitchLanguage() or 'nederlands' def readpitches(text): """Reads pitches from text.""" result = [] for pitch, octave in re.findall(r"([a-z]+)([,']*)", text): r = ly.pitch.pitchReader(language)(pitch) if r: result.append(ly.pitch.Pitch(*r, octave=ly.pitch.octaveToNum(octave))) return result def validate(text): """Returns whether the text contains exactly two pitches.""" return len(readpitches(text)) == 2 text = inputdialog.getText(mainwindow, _("Transpose"), _( "Please enter two absolute pitches, separated by a space, " "using the pitch name language \"{language}\"." ).format(language=language), icon = icons.get('tools-transpose'), help = transpose_help, validate = validate) if text == None: return transposer = ly.pitch.Transposer(*readpitches(text)) selection = cursor.hasSelection() if selection: start = cursor.selectionStart() cursor.setPosition(cursor.selectionEnd()) cursor.setPosition(0, QTextCursor.KeepAnchor) source = tokeniter.Source.selection(cursor, True) else: source = tokeniter.Source.document(cursor, True) pitches = PitchIterator(source) psource = pitches.pitches() class gen(object): def __init__(self): self.inSelection = not selection def __iter__(self): return self def __next__(self): while True: t = next(psource) if isinstance(t, (ly.lex.Space, ly.lex.Comment)): continue elif not self.inSelection and pitches.position(t) >= start: self.inSelection = True # Handle stuff that's the same in relative and absolute here if t == "\\relative": relative() elif isinstance(t, ly.lex.lilypond.MarkupScore): absolute(context()) elif isinstance(t, ly.lex.lilypond.ChordMode): chordmode() elif isinstance(t, ly.lex.lilypond.PitchCommand): if t == "\\transposition": next(psource) # skip pitch elif t == "\\transpose": for p in getpitches(context()): transpose(p) elif t == "\\key": for p in getpitches(context()): transpose(p, 0) else: return t else: return t next = __next__ tsource = gen() def context(): """Consume tokens till the level drops (we exit a construct).""" depth = source.state.depth() for t in tsource: yield t if source.state.depth() < depth: return def consume(): """Consume tokens from context() returning the last token, if any.""" t = None for t in context(): pass return t def transpose(p, resetOctave = None): """Transpose absolute pitch, using octave if given.""" transposer.transpose(p) if resetOctave is not None: p.octave = resetOctave if tsource.inSelection: pitches.write(p, editor) def chordmode(): """Called inside \\chordmode or \\chords.""" for p in getpitches(context()): transpose(p, 0) def absolute(tokens): """Called when outside a possible \\relative environment.""" for p in getpitches(tokens): transpose(p) def relative(): """Called when \\relative is encountered.""" def transposeRelative(p, lastPitch): """Transposes a relative pitch; returns the pitch in absolute form.""" # absolute pitch determined from untransposed pitch of lastPitch p.makeAbsolute(lastPitch) if not tsource.inSelection: return p # we may change this pitch. Make it relative against the # transposed lastPitch. try: last = lastPitch.transposed except AttributeError: last = lastPitch # transpose a copy and store that in the transposed # attribute of lastPitch. Next time that is used for # making the next pitch relative correctly. newLastPitch = p.copy() transposer.transpose(p) newLastPitch.transposed = p.copy() if p.octaveCheck is not None: p.octaveCheck = p.octave p.makeRelative(last) if relPitch: # we are allowed to change the pitch after the # \relative command. lastPitch contains this pitch. lastPitch.octave += p.octave p.octave = 0 pitches.write(lastPitch, editor) del relPitch[:] pitches.write(p, editor) return newLastPitch lastPitch = None relPitch = [] # we use a list so it can be changed from inside functions # find the pitch after the \relative command t = next(tsource) if isinstance(t, Pitch): lastPitch = t if tsource.inSelection: relPitch.append(lastPitch) t = next(tsource) else: lastPitch = Pitch.c1() while True: # eat stuff like \new Staff == "bla" \new Voice \notes etc. if isinstance(source.state.parser(), ly.lex.lilypond.ParseTranslator): t = consume() elif isinstance(t, ly.lex.lilypond.NoteMode): t = next(tsource) else: break # now transpose the relative expression if t in ('{', '<<'): # Handle full music expression { ... } or << ... >> for t in context(): if t == '\\octaveCheck': for p in getpitches(context()): lastPitch = p.copy() del relPitch[:] if tsource.inSelection: transposer.transpose(p) lastPitch.transposed = p pitches.write(p, editor) elif isinstance(t, ly.lex.lilypond.ChordStart): chord = [lastPitch] for p in getpitches(context()): chord.append(transposeRelative(p, chord[-1])) lastPitch = chord[:2][-1] # same or first elif isinstance(t, Pitch): lastPitch = transposeRelative(t, lastPitch) elif isinstance(t, ly.lex.lilypond.ChordStart): # Handle just one chord for p in getpitches(context()): lastPitch = transposeRelative(p, lastPitch) elif isinstance(t, Pitch): # Handle just one pitch transposeRelative(token, lastPitch) # Do it! try: with qutil.busyCursor(): with cursortools.Editor() as editor: absolute(tsource) except ly.pitch.PitchNameNotAvailable: QMessageBox.critical(mainwindow, app.caption(_("Transpose")), _( "Can't perform the requested transposition.\n\n" "The transposed music would contain quarter-tone alterations " "that are not available in the pitch language \"{language}\"." ).format(language = pitches.language))