diff --git a/engine/api.py b/engine/api.py index d4cf081..c485a1e 100644 --- a/engine/api.py +++ b/engine/api.py @@ -25,6 +25,7 @@ import logging; logging.info("import {}".format(__file__)) #Python Standard Library import sys import random +from typing import Iterable, Callable, Tuple #Third Party Modules @@ -2325,6 +2326,11 @@ def midiRelativeChannelReset(): _applyToSelection("midiRelativeChannelReset") else: _applyToItem("midiRelativeChannelReset") + +#Lilypond + +def lilypondText(text): + insertItem(items.LilypondText(text)) def exportLilypond(absoluteFilePath): lilypond.saveAsLilypond(session.data, absoluteFilePath) @@ -2332,6 +2338,27 @@ def exportLilypond(absoluteFilePath): def showPDF(): lilypond.saveAsLilypondPDF(session.data, openPDF = True) +def getLilypondBarlineList()->Iterable[Tuple[str, Callable]]: + """Return a list of very similar functions for a convenience menu in a GUI. + They are trivial so we don't need to create seperate api function for them individually. + the qt gui submenus ChooseOne uses this format.""" + + return [ + #("Bold", lambda: lilypondText('\\bar "."')), + ("Double", lambda: lilypondText('\\bar "||"')), + #("Bold Double", lambda: lilypondText('\\bar ".."')), + ("Open", lambda: lilypondText('\\bar ".|"')), + ("End/Open", lambda: lilypondText('\\bar "|.|"')), + ("End", lambda: lilypondText('\\bar "|."')), + ] + +def getLilypondRepeatList()->Iterable[Tuple[str, Callable]]: + return [ + ("Open", lambda: lilypondText('\\bar ".|:"')), + ("Close/Open", lambda: lilypondText('\\bar ":|.|:"')), + ("Close", lambda: lilypondText('\\bar ":|."')), + ] + #Debug def printPitches(): track = session.data.currentTrack() diff --git a/engine/items.py b/engine/items.py index 6d72d9f..8b1f436 100644 --- a/engine/items.py +++ b/engine/items.py @@ -1,4 +1,4 @@ -#! /usr/bin/env python3 + # -*- coding: utf-8 -*- """ Copyright 2017, Nils Hilbricht, Germany ( https://www.hilbricht.net ) @@ -565,15 +565,21 @@ class Duration(object): def lilypond(self): """Called by note.lilypond(), See Item.lilypond for the general docstring. returns a number as string.""" + + if self.durationKeyword == D_TIE: + append = "~" + else: + append = "" + n = self.genericNumber if n == 0: - return "\\breve" + return "\\breve" + append elif n == -1: - return "\\longa" + return "\\longa" + append elif n == -2: - return "\\maxima" + return "\\maxima" + append else: - return str(n) + self.dots*"." + return str(n) + self.dots*"." + append class DurationGroup(object): """Holds several durations and returns values meant for chords. @@ -661,9 +667,8 @@ class Item(object): "explicit" : False, } - def _secondInit(self, parentBlock): - """see Score._secondInit""" + """see Score._secondInit""" def deserializeDurationAndNotelistInPlace(self, serializedObject): """Part of instanceFromSerializedData. @@ -686,7 +691,7 @@ class Item(object): every child class!""" assert cls.__name__ == serializedObject["class"] self = cls.__new__(cls) - self.deserializeDurationAndNotelistInPlace(serializedObject) + self.deserializeDurationAndNotelistInPlace(serializedObject) #creates parentBlocks self._secondInit(parentTrack = parentObject) return self @@ -2822,3 +2827,52 @@ class RecordedNote(Item): def _lilypond(self): """absolutely not""" return "" + +class LilypondText(Item): + """lilypond text as a last resort""" + + def __init__(self, text): + super().__init__() + self.text = text + self._secondInit(parentBlock = None) #On item creation there is no parentBlock. The block adds itself to the item during insert. + + def _secondInit(self, parentBlock): + """see Item._secondInit""" + super()._secondInit(parentBlock) #Item._secondInit + + def _lilypond(self): + """called by block.lilypond(), returns a string. + Don't create white-spaces yourself, this is done by the structures. + When in doubt prefer functionality and robustness over 'beautiful' lilypond syntax.""" + return self.text + + @classmethod + def instanceFromSerializedData(cls, serializedObject, parentObject): + """see Score.instanceFromSerializedData""" + assert cls.__name__ == serializedObject["class"] + self = cls.__new__(cls) + self.text = serializedObject["text"] + self.deserializeDurationAndNotelistInPlace(serializedObject) #creates parentBlocks + self._secondInit(parentBlock = parentObject) + return self + + def serialize(self): + result = super().serialize() #call this in child classes + result["text"] = self.text + return result + + def _copy(self): + """return an independent copy of self""" + new = LilypondText(self.text) + return new + + + def _exportObject(self, trackState): + return { + "type" : "LilypondText", + "completeDuration" : 0, + "tickindex" : trackState.tickindex, #we parse the tickindex after we stepped over the item. + "midiBytes" : [], + "text" : self.text, + "UIstring" : self.text, #this is for a UI, possibly a text UI, maybe for simple items of a GUI. Make it as short and unambigious as possible. + } diff --git a/engine/main.py b/engine/main.py index 2182389..92b1321 100644 --- a/engine/main.py +++ b/engine/main.py @@ -73,6 +73,7 @@ class Data(template.engine.sequencer.Score): return tr def trackById(self, trackId): + """Also looks up hidden and deleted tracks""" try: ret = Track.allTracks[trackId] except: @@ -835,10 +836,11 @@ class Data(template.engine.sequencer.Score): def allItems(self, hidden = True, removeContentLinkedData = False): seenItems = set() if hidden: - what = Track.allTracks.values() #this includes hidden tracks + what = list(self.hiddenTracks.keys()) + self.tracks + #what = Track.allTracks.values() #this includes deleted tracks else: what = self.tracks - + for track in what: for block in track.blocks: for item in block.data: @@ -1001,8 +1003,10 @@ class Data(template.engine.sequencer.Score): def removeEmptyBlocks(self): dictOfTrackIdsWithListOfBlockIds = {} # [trackId] = [listOfBlockIds] - - for trId, track in Track.allTracks.items(): + + #for trId, track in Track.allTracks.items(): + for track in list(self.hiddenTracks.keys()) + self.tracks: + trId = id(track) listOfBlockIds = track.asListOfBlockIds() for block in track.blocks: if not block.data and len(track.blocks) > 1: @@ -1039,8 +1043,8 @@ class Data(template.engine.sequencer.Score): assert cls.__name__ == serializedData["class"] self = cls.__new__(cls) super().copyFromSerializedData(parentSession, serializedData, self) #Tracks, parentSession and tempoMap - self.parentSession = parentSession - self.hiddenTracks = {Track.instanceFromSerializedData(track, parentData = self):originalIndex for track, originalIndex in serializedData["hiddenTracks"]} + self.parentSession = parentSession + self.hiddenTracks = {Track.instanceFromSerializedData(parentData=self, serializedData=track):originalIndex for track, originalIndex in serializedData["hiddenTracks"]} self.tempoTrack = TempoTrack.instanceFromSerializedData(serializedData["tempoTrack"], parentData = self) self.metaData = serializedData["metaData"] self.currentMetronomeTrack = self.tracks[serializedData["currentMetronomeTrackIndex"]] diff --git a/qtgui/designer/mainwindow.py b/qtgui/designer/mainwindow.py index 32056e0..214f3d6 100644 --- a/qtgui/designer/mainwindow.py +++ b/qtgui/designer/mainwindow.py @@ -2,12 +2,13 @@ # Form implementation generated from reading ui file 'mainwindow.ui' # -# Created by: PyQt5 UI code generator 5.11.3 +# Created by: PyQt5 UI code generator 5.12.1 # # WARNING! All changes made in this file will be lost! from PyQt5 import QtCore, QtGui, QtWidgets + class Ui_MainWindow(object): def setupUi(self, MainWindow): MainWindow.setObjectName("MainWindow") @@ -437,6 +438,12 @@ class Ui_MainWindow(object): self.actionRandom_in_scale_in_octave_around_cursor.setObjectName("actionRandom_in_scale_in_octave_around_cursor") self.actionCreate_pool_from_selection = QtWidgets.QAction(MainWindow) self.actionCreate_pool_from_selection.setObjectName("actionCreate_pool_from_selection") + self.actionLyBarline = QtWidgets.QAction(MainWindow) + self.actionLyBarline.setObjectName("actionLyBarline") + self.actionLyFree_Instruction = QtWidgets.QAction(MainWindow) + self.actionLyFree_Instruction.setObjectName("actionLyFree_Instruction") + self.actionLyRepeat = QtWidgets.QAction(MainWindow) + self.actionLyRepeat.setObjectName("actionLyRepeat") self.menuObjects.addAction(self.actionMetrical_Instruction) self.menuObjects.addAction(self.actionClef) self.menuObjects.addAction(self.actionKey_Signature) @@ -612,6 +619,10 @@ class Ui_MainWindow(object): self.menuToolbox.addAction(self.menuMIDI.menuAction()) self.menuLilypond.addAction(self.actionShow_PDF) self.menuLilypond.addAction(self.actionExport_to_Ly) + self.menuLilypond.addSeparator() + self.menuLilypond.addAction(self.actionLyBarline) + self.menuLilypond.addAction(self.actionLyRepeat) + self.menuLilypond.addAction(self.actionLyFree_Instruction) self.menubar.addAction(self.menuView.menuAction()) self.menubar.addAction(self.menuEdit_2.menuAction()) self.menubar.addAction(self.menu.menuAction()) @@ -953,4 +964,8 @@ class Ui_MainWindow(object): self.actionRandom_in_scale_in_cursor_plus_octave.setText(_translate("MainWindow", "Random in-scale in cursor plus octave (authentic mode)")) self.actionRandom_in_scale_in_octave_around_cursor.setText(_translate("MainWindow", "Random in-scale in octave around cursor (hypo mode)")) self.actionCreate_pool_from_selection.setText(_translate("MainWindow", "Create pool from selection")) + self.actionLyBarline.setText(_translate("MainWindow", "Barline")) + self.actionLyFree_Instruction.setText(_translate("MainWindow", "Free Instruction")) + self.actionLyRepeat.setText(_translate("MainWindow", "Repeat")) + diff --git a/qtgui/designer/mainwindow.ui b/qtgui/designer/mainwindow.ui index 46471bd..5ca53ae 100644 --- a/qtgui/designer/mainwindow.ui +++ b/qtgui/designer/mainwindow.ui @@ -315,6 +315,10 @@ + + + + @@ -1669,6 +1673,21 @@ Create pool from selection + + + Barline + + + + + Free Instruction + + + + + Repeat + + diff --git a/qtgui/items.py b/qtgui/items.py index d67f028..bbaf3ee 100644 --- a/qtgui/items.py +++ b/qtgui/items.py @@ -28,6 +28,10 @@ from .constantsAndConfigs import constantsAndConfigs from template.qtgui.helper import stretchRect import engine.api as api + +from template.engine import pitch + + """All svg items (execept pitch-related) graphics are expected to be already on the correct position and will be inserter ON the middle line That means g-clef needs to be 14 pxiels below the y=0 coordinate since @@ -667,7 +671,7 @@ class GuiKeySignature(GuiItem): self.bigNatural.setParentItem(self) self.bigNatural.setPos(constantsAndConfigs.magicPixel, constantsAndConfigs.stafflineGap * -1) - self.rootGlyph = QtWidgets.QGraphicsSimpleTextItem(constantsAndConfigs.baseNotes[self.staticItem["root"]]) + self.rootGlyph = QtWidgets.QGraphicsSimpleTextItem(pitch.baseNotesToBaseNames[self.staticItem["root"]]) self.rootGlyph.setParentItem(self) self.rootGlyph.setPos(constantsAndConfigs.negativeMagicPixel, 2*constantsAndConfigs.stafflineGap) @@ -916,7 +920,7 @@ def staticItem2Item(staticItem): return GuiMetricalInstruction(staticItem) elif typ is "BlockEndMarker": return GuiBlockEndMarker(staticItem) - elif typ in ("InstrumentChange", "ChannelChange",): - return GuiGenericText(staticItem) + elif typ in ("InstrumentChange", "ChannelChange", "LilypondText"): + return GuiGenericText(staticItem) else: raise ValueError("Unknown Item Type:", staticItem) diff --git a/qtgui/menu.py b/qtgui/menu.py index 47beed9..b80a7f3 100644 --- a/qtgui/menu.py +++ b/qtgui/menu.py @@ -33,7 +33,7 @@ from PyQt5 import QtCore, QtGui, QtWidgets import engine.api as api from midiinput.stepmidiinput import stepMidiInput #singleton instance from .constantsAndConfigs import constantsAndConfigs -from .submenus import SecondaryClefMenu, SecondaryKeySignatureMenu, SecondaryDynamicsMenu, SecondaryMetricalInstructionMenu, SecondaryTempoChangeMenu, SecondaryTemporaryTempoChangeMenu, SecondarySplitMenu, TransposeMenu, pedalNoteChooser, SecondaryProperties, SecondaryProgramChangeMenu, SecondaryChannelChangeMenu +from .submenus import SecondaryClefMenu, SecondaryKeySignatureMenu, SecondaryDynamicsMenu, SecondaryMetricalInstructionMenu, SecondaryTempoChangeMenu, SecondaryTemporaryTempoChangeMenu, SecondarySplitMenu, TransposeMenu, pedalNoteChooser, SecondaryProperties, SecondaryProgramChangeMenu, SecondaryChannelChangeMenu, ChooseOne, forwardText class ModalKeys(object): def __init__(self): @@ -294,6 +294,12 @@ class MenuActionDatabase(object): #Midi self.mainWindow.ui.actionInstrument_Change: SecondaryProgramChangeMenu(self.mainWindow), #no lambda for submenus. They get created here once and have a __call__ option that executes them. There is no internal state in these menus. self.mainWindow.ui.actionChannel_Change: SecondaryChannelChangeMenu(self.mainWindow), #no lambda for submenus. They get created here once and have a __call__ option that executes them. There is no internal state in these menus. + + #Lilypond + #Print and Export is in self.actions + self.mainWindow.ui.actionLyBarline: ChooseOne(self.mainWindow, "Choose a Barline", api.getLilypondBarlineList()), + self.mainWindow.ui.actionLyRepeat: ChooseOne(self.mainWindow, "Choose a Repeat", api.getLilypondRepeatList()), + self.mainWindow.ui.actionLyFree_Instruction: lambda: forwardText(self.mainWindow, "Enter Instruction", api.lilypondText), } self.modalActions = { #these are only available in Note Edit Mode, not in CC Edit Mode etc. diff --git a/qtgui/scoreview.py b/qtgui/scoreview.py index c6c2e30..0f5513d 100644 --- a/qtgui/scoreview.py +++ b/qtgui/scoreview.py @@ -31,7 +31,7 @@ from PyQt5 import QtCore, QtGui, QtWidgets, QtOpenGL from template.helper import onlyOne #Our Modules - +from .submenus import GridRhytmEdit from .constantsAndConfigs import constantsAndConfigs from .structures import GuiScore import engine.api as api diff --git a/qtgui/submenus.py b/qtgui/submenus.py index 25262fe..5e7c3a7 100644 --- a/qtgui/submenus.py +++ b/qtgui/submenus.py @@ -20,6 +20,8 @@ You should have received a copy of the GNU General Public License along with this program. If not, see . """ +from typing import Iterable, Callable, Tuple + from PyQt5 import QtCore, QtGui, QtWidgets import engine.api as api @@ -149,10 +151,10 @@ class TickWidget(QtWidgets.QDialog): class Submenu(QtWidgets.QDialog): #TODO: instead of using a QDialog we could use a QWidget and use it as proxy widget on the graphic scene, placing the menu where the input cursor is. def __init__(self, mainWindow, labelString): - super().__init__(mainWindow) #if you don't set the parent to the main window the whole screen will be the root and the dialog pops up in the middle of it. + super().__init__(mainWindow) #if you don't set the parent to the main window the whole screen will be the root and the dialog pops up in the middle of it. #self.setModal(True) #we don't need this when called with self.exec() instead of self.show() self.layout = QtWidgets.QFormLayout() - #self.layout = QtWidgets.QVBoxLayout() + #self.layout = QtWidgets.QVBoxLayout() self.setLayout(self.layout) label = QtWidgets.QLabel(labelString) #"Choose a clef" or so. @@ -180,14 +182,20 @@ class Submenu(QtWidgets.QDialog): except AttributeError: super().keyPressEvent(event) + def showEvent(self, event): + #TODO: not optimal but better than nothing. + super().showEvent(event) + #self.resize(self.layout.geometry().width(), self.layout.geometry().height()) + self.resize(self.childrenRect().height(), self.childrenRect().width()) + self.updateGeometry() + def abortHandler(self): pass def __call__(self): - """This instance can be called like a function""" + """This instance can be called like a function""" self.exec() #blocks until the dialog gets closed - """ Most submenus have the line "lambda, r, value=value"... the r is the return value we get automatically from the Qt buttons which need to be handled. @@ -280,6 +288,22 @@ class SecondaryMetricalInstructionMenu(Submenu): button.clicked.connect(function) button.clicked.connect(self.done) +class ChooseOne(Submenu): + """A generic submenu that presents a list of options to the users. + Only supports up to ten entries, for number shortcuts""" + def __init__(self, mainWindow, title:str, lst:Iterable[Tuple[str, Callable]]): + if len(lst) > 9: + raise ValueError(f"ChooseOne submenu supports up to nine entries. You have {len(lst)}") + super().__init__(mainWindow, title) + + for number, (prettyname, function) in enumerate(lst): + button = QtWidgets.QPushButton(f"[{number+1}] {prettyname}") + button.setShortcut(QtGui.QKeySequence(str(number+1))) + button.setStyleSheet("Text-align:left; padding: 5px;"); + self.layout.addWidget(button) + button.clicked.connect(function) + button.clicked.connect(self.done) + class SecondaryTempoChangeMenu(Submenu): """A single tempo change where the user can decide which reference unit and how many of them per minute. @@ -610,3 +634,16 @@ def pedalNoteChooser(mainWindow): for baseDuration, v in constantsAndConfigs.commonNotes: if v == rhythmString[0]: api.pedalNotes(baseDuration) + +def forwardText(mainWindow, title, function): + text, status = QtWidgets.QInputDialog.getText(mainWindow, title, title) + if status: + function(text) + + + + + + + +