From 4d1e5646a557d002f4e8669450da489a1a659365 Mon Sep 17 00:00:00 2001 From: Nils Date: Mon, 27 Jun 2022 18:51:51 +0200 Subject: [PATCH] add initial area for signatures --- CHANGELOG | 1 + engine/api.py | 57 +++++++++++++++++++++++++++++----- engine/lilypond.py | 2 +- engine/main.py | 2 +- engine/track.py | 59 +++++++++++++++++++++++++----------- qtgui/customkeysignature.py | 17 +++++++++-- qtgui/items.py | 13 ++++---- qtgui/mainwindow.py | 5 ++- qtgui/musicstructures.py | 37 ++++++++++++++++++---- qtgui/scorescene.py | 8 +++-- qtgui/submenus.py | 10 ++++-- qtgui/trackEditor.py | 35 +++++++++++++-------- template/qtgui/mainwindow.py | 2 +- 13 files changed, 187 insertions(+), 61 deletions(-) diff --git a/CHANGELOG b/CHANGELOG index 9d45467..673ba73 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -7,6 +7,7 @@ External contributors notice at the end of the line: (LastName, FirstName / nick ## 2022-07-15 2.1.0 New function: custom key signature for any combination. +Add area in the GUI to set initial key signature, metrical instructions and clefs, accessed through the track editor Add two new functions to paste directly transposed (modal and real transposition) When not in F4-StepInput Mode use midi keyboard as pitch-cursor. Add midi-in selector drop down, as seen in Tembro and Fluajho. diff --git a/engine/api.py b/engine/api.py index a5b8122..665c5c3 100644 --- a/engine/api.py +++ b/engine/api.py @@ -628,6 +628,12 @@ def newEmptyTrack(): """Append an empty track and switch to the new track""" newIndex = len(session.data.tracks) newTrack = Track(session.data) + + if newIndex > 0: + newTrack.initialKeySignature = session.data.tracks[0].initialKeySignature.copy() + newTrack.initialMetricalInstruction = session.data.tracks[0].initialMetricalInstruction.copy() + newTrack.upbeatInTicks = session.data.tracks[0].upbeatInTicks #just an int + insertTrack(newIndex, newTrack) #handles callbacks and undo return (id(newTrack)) @@ -1851,6 +1857,20 @@ def insertKeySignature(root:int, scheme:list, lilypondOverride:str=None): keysig.lilypondParameters["override"] = lilypondOverride insertItem(keysig) +def setInitialKeySignatures(root:int, scheme:list, lilypondOverride:str=None): + """Variation of insertKeySignature to set the initial key sig for ALL tracks""" + keysig = items.KeySignature(root, scheme) + if lilypondOverride: + keysig.lilypondParameters["override"] = lilypondOverride + + for track in session.data.tracks: + track.initialKeySignature = keysig.copy() + + callbacks._tracksChanged() + for trId in session.data.listOfTrackIds(): + callbacks._updateTrack(trId) #create content: music items + callbacks._setCursor() #In case we have a different accidental for the pitch cursor + def commonKeySignaturesAsList(): """For the GUIs convenience so they can populate their drop down lists. @@ -1928,6 +1948,23 @@ def insertMetricalInstruction(treeOfInstructions, lilypondOverride = None): item.lilypondParameters["override"] = lilypondOverride insertItem(item) + +def setInitialMetricalInstruction(treeOfInstructions, lilypondOverride:str=None): + """Variation of insertMetricalInstruction to set the initial one for ALL tracks""" + item = items.MetricalInstruction(treeOfInstructions) + + if lilypondOverride: + item.lilypondParameters["override"] = lilypondOverride + + for track in session.data.tracks: + track.initialMetricalInstruction = item.copy() + + callbacks._tracksChanged() + for trId in session.data.listOfTrackIds(): + callbacks._updateTrack(trId) #create content: music items + callbacks._setCursor() #In case we have a different accidental for the pitch cursor + + def commonMetricalInstructionsAsList(): """for musical reasons 5/4 and 7/8 and other a-symetrical metrical instructions cannot be in here since it is unknown which @@ -1946,7 +1983,7 @@ def commonMetricalInstructionsAsList(): "3/2", ] -def insertCommonMetricalInstrucions(scheme): +def insertCommonMetricalInstrucions(scheme, setInitialInsteadCursorInsert=False): """A metrical instruction requires a lilypond override. You can use them without of course but they will not work when exported.""" schemes = { @@ -1973,7 +2010,11 @@ def insertCommonMetricalInstrucions(scheme): "1/1" : "\\cadenzaOff \\time 1/1", "3/2" : "\\cadenzaOff \\time 3/2", } - insertMetricalInstruction(schemes[scheme], lilypondOverride = lilypond[scheme]) + + if setInitialInsteadCursorInsert: + setInitialMetricalInstruction(schemes[scheme], lilypondOverride = lilypond[scheme]) + else: #item insert + insertMetricalInstruction(schemes[scheme], lilypondOverride = lilypond[scheme]) def metricalTest(): @@ -2723,7 +2764,7 @@ def lilypondText(text:str): try: insertItem(items.LilypondText(text)) except Exception as err: - logger.error(err) + logger.error(f"Text Item Lilypond Error for '{text}': " + err) def lilypondMark(text:str, aboveStaff=True): """This is actual human-readable text, called a Mark by lilypond. @@ -2747,16 +2788,18 @@ def lilypondMark(text:str, aboveStaff=True): def exportLilypond(absoluteFilePath): save() lilypond.saveAsLilypond(session.data, absoluteFilePath) - #try: - # lilypond.saveAsLilypond(session.data, absoluteFilePath) - #except Exception as err: - # logger.error(err) + try: + lilypond.saveAsLilypond(session.data, absoluteFilePath) + except Exception as err: + logger.error(err) def showPDF(): try: save() lilypond.saveAsLilypondPDF(session.data, openPDF = True) except Exception as err: + logger.error("PDF/Lilypond Exception. The following is a PYTHON, not a lilypond error:") + print (err) logger.error(err) diff --git a/engine/lilypond.py b/engine/lilypond.py index bc5d22f..62cd515 100644 --- a/engine/lilypond.py +++ b/engine/lilypond.py @@ -41,7 +41,7 @@ da = date.fromordinal(730920) # 730920th day after 1. 1. 0001 def saveAsLilypond(score, absoluteFilePath = None): #absoluteFilePath = self.absoluteFilePath + ".ly" if (not absoluteFilePath) else absoluteFilePath - assert absoluteFilePath.endswith(".ly") + assert absoluteFilePath.endswith(".ly"), absoluteFilePath result = score.lilypond() if result: with open(absoluteFilePath, "w", encoding="utf-8") as f: diff --git a/engine/main.py b/engine/main.py index 059a319..33abbab 100644 --- a/engine/main.py +++ b/engine/main.py @@ -1056,7 +1056,7 @@ class Data(template.engine.sequencer.Score): Only visible tracks get exported. Each object controls what it exports. Overrides are possible at every level. """ - tempoStaff = self.tempoTrack.lilypond() if self.metaData["metronome"] else "" + tempoStaff = self.tempoTrack.lilypond() if "metronome" in self.metaData and self.metaData["metronome"] else "" data = {track:track.lilypond() for track in self.tracks} #processed in the lilypond module return fromTemplate(session = self.parentSession, templateFile = "default.ly", data = data, meta = self.metaData, tempoStaff = tempoStaff) diff --git a/engine/track.py b/engine/track.py index a0fa912..bb833d8 100644 --- a/engine/track.py +++ b/engine/track.py @@ -273,10 +273,7 @@ class TrackState(object): """TrackState stays not the same for one track but gets recreated, at least in track.head(). This means init is always position 0.""" - defaultClef = Clef("treble") defaultDynamicSignature = DynamicSignature("custom") #This is the start dynamic value like 'forte', not the DynamicSettingsSignature - defaultMetricalInstruction = MetricalInstruction(tuple(), isMetrical = False) - defaultKeySignature = KeySignature(20, [0,0,0,0,0,0,0]) def __init__(self, track): self.track = track @@ -286,10 +283,9 @@ class TrackState(object): self.ticksSinceLastMeasureStart = 0 #Only for export. self.barlines = [] #a list of ints/tickpositions. These are the non-editable, non-movable single barlines. #stacks. There is always one item left in the stack, the default: - self.keySignatures = [self.defaultKeySignature] #C Major - #self.clefs = [self.defaultClef] + self.keySignatures = [track.initialKeySignature] self.clefs = [Clef(track.initialClefKeyword)] - self.metricalInstructions = [self.defaultMetricalInstruction] #no barlines, no metrical information. + self.metricalInstructions = [track.initialMetricalInstruction] #no barlines, no metrical information. self.dynamicSignatures = [self.defaultDynamicSignature] self.dynamicRamp = None #Not for cursor movement so it is not a stack. self.EXPORTtiedNoteExportObjectsWaitingForClosing = {} #pitch:noteExportObject . Empty during cursor movement, filled during export. @@ -401,9 +397,8 @@ class Track(object): #Since version 2.1.0 the initial signature can be set by the user, mostly as a GUI convenience. #These are used by the track-state init/head self.initialClefKeyword:str = "treble" #it is much easier to handle the keyword with save/load and setting this value, so we are not using the item.Clef class - - #self.initialMetricalInstruction = MetricalInstruction(tuple(), isMetrical = False) - #self.initialKeySignature = KeySignature(20, [0,0,0,0,0,0,0]) + self.initialKeySignature = KeySignature(20, [0,0,0,0,0,0,0]) #C Major + self.initialMetricalInstruction = MetricalInstruction(tuple(), isMetrical = False) self.asMetronomeData = None #This track as metronome version. Is always up to date through export. @@ -842,21 +837,36 @@ class Track(object): This dict begins here. Each track gets its own. """ + + carryLilypondRanges = {} #handed from item to item for ranges such as tuplets. Can act like a stack or simply remember stuff. + + #Initial Metrical Instruction for item in self.blocks[0].data[:4]: - if type(item) is MetricalInstruction: + if type(item) is MetricalInstruction: #don't use the initial. use the user provided one. timeSig = "" break else: - timeSig = "\\once \\override Staff.TimeSignature #'stencil = ##f \\cadenzaOn\n" + if self.initialMetricalInstruction.treeOfInstructions: + timeSig = self.initialMetricalInstruction.lilypond(carryLilypondRanges) + "\n" + else: + timeSig = "\\once \\override Staff.TimeSignature #'stencil = ##f \\cadenzaOn\n" + #Initial Clef for item in self.blocks[0].data[:4]: - if type(item) is Clef: + if type(item) is Clef: #don't use the initial. use the user provided one. clef = "" break else: clef = "\\clef " + self.initialClefKeyword + "\n" #internal clef keywords are the same as lilypond - carryLilypondRanges = {} #handed from item to item for ranges such as tuplets. Can act like a stack or simply remember stuff. + #Initial Key Signature + for item in self.blocks[0].data[:4]: + if type(item) is KeySignature: #don't use the initial. use the user provided one. + keySignature = "" + break + else: + keySignature = self.initialKeySignature.lilypond(carryLilypondRanges) + "\n" + upbeatLy = "\\partial {} ".format(Duration.createByGuessing(self.upbeatInTicks).lilypond(carryLilypondRanges)) if self.upbeatInTicks else "" @@ -911,7 +921,7 @@ class Track(object): pass if lyData: - return clef + timeSig + upbeatLy + lyData + "\n" + return clef + keySignature + timeSig + upbeatLy + lyData + "\n" else: return "" #Empty track @@ -926,6 +936,8 @@ class Track(object): "double" : self.double, "initialClefKeyword" : self.initialClefKeyword, + "initialKeySignature" : self.initialKeySignature.serialize(), + "initialMetricalInstruction" : self.initialMetricalInstruction.serialize(), "initialMidiChannel" : self.initialMidiChannel, "initialMidiProgram" : self.initialMidiProgram, "initialMidiBankMsb" : self.initialMidiBankMsb, @@ -947,8 +959,8 @@ class Track(object): self.upbeatInTicks = int(serializedData["upbeatInTicks"]) self.blocks = [Block.instanceFromSerializedData(block, parentObject = self) for block in serializedData["blocks"]] - self.durationSettingsSignature = DurationSettingsSignature.instanceFromSerializedData(serializedData["durationSettingsSignature"], parentObject = self) - self.dynamicSettingsSignature = DynamicSettingsSignature.instanceFromSerializedData(serializedData["dynamicSettingsSignature"], parentObject = self) + self.durationSettingsSignature = DurationSettingsSignature.instanceFromSerializedData(serializedData["durationSettingsSignature"], parentObject=self) + self.dynamicSettingsSignature = DynamicSettingsSignature.instanceFromSerializedData(serializedData["dynamicSettingsSignature"], parentObject=self) self.double = serializedData["double"] self.initialMidiChannel = serializedData["initialMidiChannel"] self.initialMidiProgram = serializedData["initialMidiProgram"] @@ -966,6 +978,16 @@ class Track(object): else: self.initialClefKeyword = "treble" + if "initialKeySignature" in serializedData: + self.initialKeySignature = KeySignature.instanceFromSerializedData(serializedData["initialKeySignature"], parentObject=self) + else: + self.initialKeySignature = KeySignature(20, [0,0,0,0,0,0,0]) #C Major + + if "initialMetricalInstruction" in serializedData: + self.initialMetricalInstruction = MetricalInstruction.instanceFromSerializedData(serializedData["initialMetricalInstruction"], parentObject=self) + else: + self.initialMetricalInstruction = MetricalInstruction(tuple(), isMetrical = False) + self._processAfterInit() return self @@ -1254,7 +1276,10 @@ class Track(object): metaData["barlines"] = barlines.keys() metaData["duration"] = self.state.tickindex #tickindex is now at the end, so this is the end duration. This includes Blocks minimumDuration as well since it is included in left/right metaData["beams"] = resultBeamGroups - metaData["initialClef"] = self.initialClefKeyword + metaData["initialClef"] = self.initialClefKeyword #it is already a string + #the state is at the end, but it doesn't matter for the init-sigs. It will show the wrong tick index, but don't worry. + metaData["initialKeySignature"] = self.initialKeySignature.exportObject(self.state) + metaData["initialMetricalInstruction"] = self.initialMetricalInstruction.exportObject(self.state) #Notes t = (midiNotesBinaryCboxData, 0, self.state.tickindex) diff --git a/qtgui/customkeysignature.py b/qtgui/customkeysignature.py index abaeaed..1db96cb 100644 --- a/qtgui/customkeysignature.py +++ b/qtgui/customkeysignature.py @@ -34,13 +34,21 @@ import engine.api as api class CustomKeySignatureWidget(QtWidgets.QDialog): - def __init__(self, mainWindow): - """Init is called everytime the dialog gets shown""" + def __init__(self, mainWindow, setInitialKeysigInsteadCursorInsert:bool=False): + """Init is called everytime the dialog gets shown. + + This function can be used to insert a normal sig at cursor position, which is the main + purpose. + But later we retrofitted the option to set the initial keysig for all tracks, + which is just a different api function. + set setInitialKeysigInsteadCursorInsert to True for this behaviour. + """ super().__init__(mainWindow) self.mainWindow = mainWindow self.ui = Ui_customKeySignature() self.ui.setupUi(self) + self.setInitialKeysigInsteadCursorInsert = setInitialKeysigInsteadCursorInsert self.ui.buttonBox.accepted.connect(self.process) self.ui.buttonBox.rejected.connect(self.reject) @@ -242,6 +250,9 @@ class CustomKeySignatureWidget(QtWidgets.QDialog): if button.isChecked(): deviationFromMajorScale.append(10*counter -20) - api.insertKeySignature(root=selectedRootPitch, scheme=deviationFromMajorScale) + if self.setInitialKeysigInsteadCursorInsert: + api.setInitialKeySignatures(root=selectedRootPitch, scheme=deviationFromMajorScale) + else: #default + api.insertKeySignature(root=selectedRootPitch, scheme=deviationFromMajorScale) self.done(True) diff --git a/qtgui/items.py b/qtgui/items.py index 1de1c3f..3046e66 100644 --- a/qtgui/items.py +++ b/qtgui/items.py @@ -668,11 +668,11 @@ class GuiKeySignature(GuiItem): #No accidentals in the keysig (C-Major, D-Dorian etc.) gets a big natural sign. self.bigNatural = GuiKeySignature.accidentals[0]() #"big natural"... hö hö hö hö self.bigNatural.setParentItem(self) - self.bigNatural.setPos(constantsAndConfigs.magicPixel, constantsAndConfigs.stafflineGap * -1) + self.bigNatural.setPos(constantsAndConfigs.magicPixel, constantsAndConfigs.stafflineGap * GuiKeySignature.pitchModYOffset[0]) self.rootGlyph = QtWidgets.QGraphicsSimpleTextItem(pitch.baseNotesToAccidentalNames[self.staticItem["root"]]) self.rootGlyph.setParentItem(self) - self.rootGlyph.setPos(constantsAndConfigs.negativeMagicPixel, 2*constantsAndConfigs.stafflineGap) + self.rootGlyph.setPos(1, 2*constantsAndConfigs.stafflineGap) accidentals = { #QGraphicsItems can only be used once so we have to save a class constructor here instead of an instance. @@ -765,7 +765,7 @@ class GuiTimeSignature(GuiItem): class GuiMetricalInstruction(GuiItem): - def __init__(self, staticItem): + def __init__(self, staticItem, drawMarker=True): super().__init__(staticItem) if staticItem["oneMeasureInTicks"] == 0: @@ -790,9 +790,10 @@ class GuiMetricalInstruction(GuiItem): self.text.setParentItem(self) self.text.setPos(-6, -7 * constantsAndConfigs.stafflineGap) #by definition the metrical sig is in the same position as the measure number. Shift high up - self.marker = GuiPositionMarker() - self.marker.setParentItem(self) - self.marker.setPos(0,0) + if drawMarker: + self.marker = GuiPositionMarker() + self.marker.setParentItem(self) + self.marker.setPos(0,0) def treeToPrettyString(self, tree): """Convert a metrical instruction into a pretty string with note symbols diff --git a/qtgui/mainwindow.py b/qtgui/mainwindow.py index 735b6ed..e78c8ff 100644 --- a/qtgui/mainwindow.py +++ b/qtgui/mainwindow.py @@ -198,7 +198,10 @@ class MainWindow(TemplateMainWindow): def toggleMainView(self): - """Switch between the Track Editor and Score/Block Editor""" + """Switch between the Track Editor and Score/Block Editor. + This can happen through the menuaction. There is another way through mouseReleaseEvent + in scoreScene, but this triggers the menu action as well. + Without menu action we don't change the checkbox.""" if self.ui.actionData_Editor.isChecked(): self.ui.mainStackWidget.setCurrentIndex(self.ui.mainStackWidget.indexOf(self.trackEditor)) diff --git a/qtgui/musicstructures.py b/qtgui/musicstructures.py index 5271e30..897f8b3 100644 --- a/qtgui/musicstructures.py +++ b/qtgui/musicstructures.py @@ -36,7 +36,7 @@ from template.qtgui.helper import stretchLine, stretchRect, callContextMenu, rem import engine.api as api from . import graphs -from .items import staticItem2Item, GuiTieCurveGraphicsItem, GuiClef +from .items import staticItem2Item, GuiTieCurveGraphicsItem, GuiClef, GuiKeySignature, GuiMetricalInstruction from .constantsAndConfigs import constantsAndConfigs from .submenus import BlockPropertiesEdit @@ -450,6 +450,34 @@ class GuiTrack(QtWidgets.QGraphicsItem): super().__init__() self.parent = parent + def createInitialSignatureArea(self, metaDataDict): + """Assumes an empty track, called only in createGraphicItemsFromData + Initial Signatures, if present. Negative X-axis values. + + The stafflines draw themselves to the left by the constant sceneXOffsetForInitialSigs. + + In scoreScene.mouseReleaseEvent we check if the mouse click was within our initial area + and call the track Editor.""" + + xPos = sceneXOffsetForInitialSigs+5 + clefSVGItem = GuiClef.clefs[metaDataDict["initialClef"]]() #just the graphics + self.anchor.addToGroup(clefSVGItem) + clefSVGItem.setPos(xPos, GuiClef.cleffYOnStaff[metaDataDict["initialClef"]]) + + xPos += clefSVGItem.boundingRect().width() + + initKeySig = GuiKeySignature(metaDataDict["initialKeySignature"]) + self.anchor.addToGroup(initKeySig) + initKeySig.setPos(xPos, 0) + xPos += initKeySig.boundingRect().width() + + #The metrical position is just text. We put that on top of the staff and remove the arrow indicator. + if metaDataDict["initialMetricalInstruction"]["oneMeasureInTicks"]: + initMetricalInstruction = GuiMetricalInstruction(metaDataDict["initialMetricalInstruction"], drawMarker=False) + self.anchor.addToGroup(initMetricalInstruction) + initMetricalInstruction.setPos(sceneXOffsetForInitialSigs +10, 0) #+10 because the GuiItem itself has a negative x offset + + def createGraphicItemsFromData(self, staticRepresentationList): """Create staff objects including simple barlines""" self.parentScore.cursor.clearItemHighlight() #or else the current highlight gets deleted while it is on an item @@ -463,11 +491,8 @@ class GuiTrack(QtWidgets.QGraphicsItem): self.anchor = GuiTrack.TrackAnchor(self) self.anchor.setParentItem(self) - metaDataDict = staticRepresentationList.pop() - #Initial Signatures, if present. Negative X-axis values - clefSVGItem = GuiClef.clefs[metaDataDict["initialClef"]]() #just the graphics - self.anchor.addToGroup(clefSVGItem) - clefSVGItem.setPos(sceneXOffsetForInitialSigs+5, GuiClef.cleffYOnStaff[metaDataDict["initialClef"]]) + metaDataDict = staticRepresentationList.pop() #we want the dict, but we also want it separated from the item list. It is, by specs, the last item. + self.createInitialSignatureArea(metaDataDict) #Real items, positive X-axis values for staticItem in staticRepresentationList: diff --git a/qtgui/scorescene.py b/qtgui/scorescene.py index b0e59b2..1909e2d 100644 --- a/qtgui/scorescene.py +++ b/qtgui/scorescene.py @@ -336,9 +336,6 @@ class GuiScore(QtWidgets.QGraphicsScene): self.updateSceneRect() #Macro-Structure: Score / Track / Block Moving and Duplicating - #Hold the shift Key to unlock the moving mode.super().keyPressEvent(event) - #No note-editing requires a mouse action, so the mouse is free for controlling other aspects. - #Like zooming or moving blocks around. """ def keyPressEvent(self, event): @@ -601,6 +598,11 @@ class GuiScore(QtWidgets.QGraphicsScene): else: api.toTickindex(trackId, event.scenePos().x() * constantsAndConfigs.ticksToPixelRatio) + elif (not event.button() == 1) and event.scenePos().x() < -25: + #This is also part of musicstructures.py createInitialSignatureArea() (Track). We set a margin against misclicks. + self.parentView.mainWindow.ui.actionData_Editor.trigger() + + super().mouseReleaseEvent(event) def reactToHorizontalScroll(self, value:int): diff --git a/qtgui/submenus.py b/qtgui/submenus.py index 5790bc8..b057e74 100644 --- a/qtgui/submenus.py +++ b/qtgui/submenus.py @@ -234,6 +234,8 @@ class SecondarySplitMenu(Submenu): class SecondaryKeySignatureMenu(Submenu): def __init__(self, mainWindow): + """Init is only called once per program, during startup""" + super().__init__(mainWindow, translate("submenus", "root note is the cursor position"), hasOkCancelButtons=2) l = [("[{}] {}".format(num+1, modeString.title()), lambda r, modeString=modeString: api.insertCursorCommonKeySignature(modeString)) for num, modeString in enumerate(api.commonKeySignaturesAsList())] @@ -249,7 +251,6 @@ class SecondaryKeySignatureMenu(Submenu): super().__call__() - class SecondaryDynamicsMenu(Submenu): def __init__(self, mainWindow): super().__init__(mainWindow, translate("submenus", "choose a dynamic"), hasOkCancelButtons=2) @@ -269,10 +270,13 @@ class SecondaryDynamicsMenu(Submenu): button.clicked.connect(self.done) class SecondaryMetricalInstructionMenu(Submenu): - def __init__(self, mainWindow): + def __init__(self, mainWindow, setInitialInsteadCursorInsert:bool=False): super().__init__(mainWindow, translate("submenus", "choose a metrical instruction"), hasOkCancelButtons=2) - l = [("[{}] {}".format(num+1, modeString), lambda r, modeString=modeString: api.insertCommonMetricalInstrucions(modeString)) for num, modeString in enumerate(api.commonMetricalInstructionsAsList())] + if setInitialInsteadCursorInsert: #different api function + l = [("[{}] {}".format(num+1, modeString), lambda r, modeString=modeString: api.insertCommonMetricalInstrucions(modeString, setInitialInsteadCursorInsert=True)) for num, modeString in enumerate(api.commonMetricalInstructionsAsList())] + else: + l = [("[{}] {}".format(num+1, modeString), lambda r, modeString=modeString: api.insertCommonMetricalInstrucions(modeString)) for num, modeString in enumerate(api.commonMetricalInstructionsAsList())] for number, (prettyname, function) in enumerate(l): button = QtWidgets.QPushButton(prettyname) diff --git a/qtgui/trackEditor.py b/qtgui/trackEditor.py index 4db9f14..f8ae4d4 100644 --- a/qtgui/trackEditor.py +++ b/qtgui/trackEditor.py @@ -22,6 +22,7 @@ along with this program. If not, see . import logging; logger = logging.getLogger(__name__); logger.info("import") #Standard Library +from contextlib import contextmanager #Third party from PyQt5 import QtCore, QtGui, QtWidgets @@ -33,8 +34,9 @@ from template.helper import pairwise #Our own files from .constantsAndConfigs import constantsAndConfigs from .designer.trackWidget import Ui_trackGroupWidget -from .submenus import TickWidget, CombinedTickWidget -from contextlib import contextmanager +from .submenus import TickWidget, CombinedTickWidget, SecondaryMetricalInstructionMenu +from .customkeysignature import CustomKeySignatureWidget +from .custommetricalinstruction import CustomMetricalInstructionWidget import engine.api as api LIST_OF_CLEF_KEYWORDS = api.getPossibleClefKeywords() #TODO: translate? But keep the keyword as index. setData @@ -69,12 +71,9 @@ class TrackWidget(QtWidgets.QGroupBox): self.ui.instrumentName.editingFinished.connect(self.nameChanged) self.ui.shortInstrumentName.editingFinished.connect(self.nameChanged) - #Set the initial clef. Unlike the metrical instruction the engine actually has a default clef, which is the treble clef - self.ui.initialClef_comboBox.addItems(LIST_OF_CLEF_KEYWORDS) #Current index is set in self.updateData self.ui.initialClef_comboBox.currentIndexChanged.connect(self.dataChanged) - #Create a menu with checkboxes to allow switching on and off of additional channels for the CC sub-track #However, we will not use normal checkable Menu actions since they close the menu after triggering. even blockSignals does not prevent closing self.ccChannels = {} @@ -354,6 +353,18 @@ class TrackEditor(QtWidgets.QWidget): allUpbeatsLayout.addWidget(self.allUpbeatsCombinedTickWidget) allUpbeatsLayout.addWidget(allUpbeatsPushButton) + #Set All Key Signature and Metrical Sig Buttons + #We cannot use the simple keysig submenu because we have no cursor. + + customKeySigButton = QtWidgets.QPushButton(translate("trackEditorPythonFile", "Key Signatures")) + customKeySigButton.clicked.connect(self.setAllCustomKeySig) + allUpbeatsLayout.addWidget(customKeySigButton) + + metricalInstructionButton = QtWidgets.QPushButton(translate("trackEditorPythonFile", "Metrical Instructions")) + metricalInstructionButton.clicked.connect(self.setAllMetricalInstruction) + allUpbeatsLayout.addWidget(metricalInstructionButton) + + #Reset all Advanced Views foldAllAdvanvced = QtWidgets.QPushButton(translate("trackEditorPythonFile", "Fold all Advanced")) foldAllAdvanvced.clicked.connect(self.foldAllAdvanvced) @@ -441,14 +452,14 @@ class TrackEditor(QtWidgets.QWidget): for trackWidget in self.tracks.values(): trackWidget.ui.advanced.setChecked(True) + def setAllCustomKeySig(self): + CustomKeySignatureWidget(self.mainWindow, setInitialKeysigInsteadCursorInsert=True) + + def setAllMetricalInstruction(self): + SecondaryMetricalInstructionMenu(self.mainWindow, setInitialInsteadCursorInsert=True).exec() + + - def keyPressEvent(self, event): - """Escape closes the track editor""" - k = event.key() #49=1, 50=2 etc. - if k == QtCore.Qt.Key_Escape: - self.mainWindow.ui.actionData_Editor.setChecked(False) - self.mainWindow.toggleMainView() - super().keyPressEvent(event) """ api.callbacks.updateBlockTrack.append(lambda trId, blocksExportData: self.tracks[trId].blockScene.regenerateBlocks(blocksExportData)) diff --git a/template/qtgui/mainwindow.py b/template/qtgui/mainwindow.py index 8e6f4a3..ec3a3c9 100644 --- a/template/qtgui/mainwindow.py +++ b/template/qtgui/mainwindow.py @@ -377,7 +377,7 @@ class MainWindow(QtWidgets.QMainWindow): This event filter somehow does not differentiate between numpad on or numpad off. There maybe is another qt check for that. """ - if (not self.menu.ui.menubar.activeAction()) and event.type() == 51 and type(event) is QtGui.QKeyEvent and event.modifiers() == QtCore.Qt.KeypadModifier and event.text() and event.text() in "0123456789": + if (not self.menu.ui.menubar.activeAction()) and event.type() == QtCore.QEvent.ShortcutOverride and type(event) is QtGui.QKeyEvent and event.modifiers() == QtCore.Qt.KeypadModifier and event.text() and event.text() in "0123456789": action = self.menu.hoverShortcutDict[event.text()] if action: action.trigger()