From f1ebbb01bc46a2d954167bfc3215fed2e6074bb8 Mon Sep 17 00:00:00 2001
From: Nils <>
Date: Tue, 23 Apr 2019 22:06:54 +0200
Subject: [PATCH] various lilypond stuff
---
engine/api.py | 27 ++++++++++++++
engine/items.py | 70 +++++++++++++++++++++++++++++++-----
engine/main.py | 16 +++++----
qtgui/designer/mainwindow.py | 17 ++++++++-
qtgui/designer/mainwindow.ui | 19 ++++++++++
qtgui/items.py | 10 ++++--
qtgui/menu.py | 8 ++++-
qtgui/scoreview.py | 2 +-
qtgui/submenus.py | 45 ++++++++++++++++++++---
9 files changed, 190 insertions(+), 24 deletions(-)
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)
+
+
+
+
+
+
+
+