Browse Source

fixes from regressions and code re-structuring

master
Nils 5 years ago
parent
commit
0c63c254bd
  1. 2
      configure
  2. 2
      documentation/out/english.html
  3. 2
      documentation/out/german.html
  4. 10
      engine/api.py
  5. 1
      engine/cursor.py
  6. 5
      engine/tempotrack.py
  7. 10
      midiinput/stepmidiinput.py
  8. 1
      qtgui/conductor.py
  9. 42
      qtgui/cursor.py
  10. 16
      qtgui/designer/mainwindow.py
  11. 22
      qtgui/designer/mainwindow.ui
  12. 256
      qtgui/grid.py
  13. 2
      qtgui/items.py
  14. 11
      qtgui/mainwindow.py
  15. 8
      qtgui/menu.py
  16. 488
      qtgui/musicstructures.py
  17. 15083
      qtgui/resources.py
  18. 498
      qtgui/scorescene.py
  19. 67
      qtgui/scoreview.py
  20. 4
      qtgui/submenus.py

2
configure

@ -1,5 +1,5 @@
#!/bin/bash
program=laborejo2 #MUST be the same as engine/config.py shortName
program=laborejo #MUST be the same as engine/config.py shortName
cboxconfigure="--without-fluidsynth --without-libsmf"
. template/configure.template #. is the posix compatible version of source

2
documentation/out/english.html

@ -730,7 +730,7 @@ The program is split in two parts. A shared "template" between the Laborejo Soft
</div>
<div id="footer">
<div id="footer-text">
Last updated 2019-10-13 01:32:13 +0200
Last updated 2019-11-01 19:13:35 +0100
</div>
</div>
</body>

2
documentation/out/german.html

@ -716,7 +716,7 @@ Ansonsten starten Sie laborejo mit diesem Befehl, Sprachcode ändern, vom Termin
</div>
<div id="footer">
<div id="footer-text">
Last updated 2019-10-13 01:32:13 +0200
Last updated 2019-11-01 19:13:35 +0100
</div>
</div>
</body>

10
engine/api.py

@ -98,7 +98,7 @@ class ClientCallbacks(Callbacks): #inherits from the templates api callbacks
if self.setCursor:
ex = session.data.cursorExport()
for func in self.setCursor:
for func in self.setCursor:
func(ex)
"""Exports a tuple of cursors. This is to indicate a GUI to
@ -182,9 +182,9 @@ class ClientCallbacks(Callbacks): #inherits from the templates api callbacks
any item data."""
#TODO: does NOT call template.api._numberOfTracksChanged
session.data.updateJackMetadataSorting()
ex = session.data.listOfStaticTrackRepresentations()
if self.tracksChanged:
ex = session.data.listOfStaticTrackRepresentations()
if self.tracksChanged:
for func in self.tracksChanged:
func(ex)
@ -301,7 +301,8 @@ def startEngine(nsmClient):
for cc in session.data.trackById(trId).listOfCCsInThisTrack():
callbacks._updateGraphTrackCC(trId, cc) #create content: CC points. user points and interpolated points.
setMetronome(session.data.currentMetronomeTrack.asMetronomeData, label=session.data.currentMetronomeTrack.name) #track.asMetronomeData is generated in staticRepresentation #template api. has callbacks
if session.data.currentMetronomeTrack.asMetronomeData:
setMetronome(session.data.currentMetronomeTrack.asMetronomeData, label=session.data.currentMetronomeTrack.name) #track.asMetronomeData is generated in staticRepresentation #template api. has callbacks
callbacks._setCursor()
global laborejoEngineStarted #makes for a convenient check. stepMidiInput uses it, which needs to know that the gui already started the api.
@ -687,6 +688,7 @@ def setTrackSettings(trId, dictionary):
callbacks._tracksChanged()
callbacks._updateSingleTrackAllCC(trId)
callbacks._updateTrack(trId)
callbacks._setCursor() #for midi channel RT thru
def resetDynamicSettingsSignature(trId):
trackObject = session.data.trackById(trId)

1
engine/cursor.py

@ -123,6 +123,7 @@ class Cursor:
"trackIndex": trackState.index(),
"track" : trackState.track,
"cboxMidiOutUuid" : trackState.track.sequencerInterface.cboxMidiOutUuid, #used for midi throught. Step midi shall produce sound through the current track.
"midiChannel" : trackState.midiChannel(), #zero based
"trackId" : id(trackState.track),
"position" : trackState.position(),
"tickindex" : trackState.tickindex,

5
engine/tempotrack.py

@ -606,13 +606,14 @@ class TempoTrack(GraphTrackCC):
offsetForBlock[block] = offsetCounter
offsetCounter += block.duration #after this block is over how many ticks have gone by?
sendToSequencerTempoMap = {} #pos:value
sendToSequencerTempoMap = {} #{positionInTicks:(bpmAsFloat, timesigUpper, timesigLower)}
for staticItem in result: #static item is a tempo change as exportDictItem from above.
value = float(abs(staticItem["value"])) #exportItems have negative values. calfbox wants a float.
tempoBlock = blocksAsDict[staticItem["blockId"]]
absoluteBlockStartPosition = offsetForBlock[tempoBlock]
pos = staticItem["positionInBlock"] + absoluteBlockStartPosition
sendToSequencerTempoMap[pos] = value
sendToSequencerTempoMap[pos] = (value, 4, 4) #TODO: faked 4/4 meter because we do not combine tempo and timesig, but midi and jack wants to combine it.
self.parentData.tempoMap.setTempoMap(sendToSequencerTempoMap)

10
midiinput/stepmidiinput.py

@ -44,6 +44,7 @@ class StepMidiInput(MidiInput):
"""
def __init__(self):
#No super init in here! This is delayed until self.start
self.firstActiveNote = None #for chord entry.
self._currentlyActiveNotes = set()
@ -69,8 +70,8 @@ class StepMidiInput(MidiInput):
return self.midiProcessor.active
except AttributeError: #during startupt
return False
def _insertMusicItemFromMidi(self, channel, midipitch, velocity):
def _insertMusicItemFromMidi(self, timeStamp, channel, midipitch, velocity):
if self._currentlyActiveNotes: #Chord
api.left()
keysig = api.session.data.currentTrack().state.keySignature()
@ -85,7 +86,7 @@ class StepMidiInput(MidiInput):
self._currentlyActiveNotes.add(midipitch)
def _pop(self, channel, midipitch, velocity):
def _pop(self, timeStamp, channel, midipitch, velocity):
self._currentlyActiveNotes.remove(midipitch)
def setMidiInputActive(self, state:bool):
@ -99,6 +100,7 @@ class StepMidiInput(MidiInput):
"""We don't need to react to deleted tracks because that does reset the cursor.
The template midi in does _not_ check if the routed output ports still exist.
however, that is a low risk state that needs changes in the program"""
self.setMidiThru(cursorExport["cboxMidiOutUuid"])
self.setMidiThru(cursorExport["cboxMidiOutUuid"])
self.setMidiThruChannel(cursorExport["midiChannel"]+1) #cursor midi channel is 0 based
stepMidiInput = StepMidiInput() #global to use in other parts of Laborejo

1
qtgui/conductor.py

@ -532,6 +532,7 @@ class TempoPoint(QtWidgets.QGraphicsItem):
def wheelEvent(self, event):
"""This buffers until hoverLeaveEvent and then the new value is sent in self.hoverLeaveEvent"""
print ("w")
if event.delta() > 0:
self.wheelEventChangedValue += 1
else:

42
qtgui/cursor.py

@ -77,14 +77,14 @@ class Cursor(QtWidgets.QGraphicsItemGroup):
Cursor.hightlightEffect.setColor(QtGui.QColor("cyan"))
Cursor.hightlightEffect.setStrength(0.7) #opacity of the effect
#Cursor.stafflineEffect = QtWidgets.QGraphicsColorizeEffect() #default strength of the effect is 1.0
#Cursor.stafflineEffect.setColor(QtGui.QColor("cyan"))
#Cursor.stafflineEffect.setStrength(1) #opacity of the effect
##Cursor.stafflineEffect = QtWidgets.QGraphicsColorizeEffect() #default strength of the effect is 1.0
##Cursor.stafflineEffect.setColor(QtGui.QColor("cyan"))
##Cursor.stafflineEffect.setStrength(1) #opacity of the effect
Cursor.stafflineEffect = QtWidgets.QGraphicsDropShadowEffect()
Cursor.stafflineEffect.setColor(QtGui.QColor("black"))
Cursor.stafflineEffect.setOffset(0,0)
Cursor.stafflineEffect.setBlurRadius(5)
#Cursor.stafflineEffect = QtWidgets.QGraphicsDropShadowEffect()
#Cursor.stafflineEffect.setColor(QtGui.QColor("black"))
#Cursor.stafflineEffect.setOffset(0,0)
#Cursor.stafflineEffect.setBlurRadius(5)
def setCursor(self, cursorExportObject):
self.cursorExportObject = cursorExportObject
@ -113,14 +113,14 @@ class Cursor(QtWidgets.QGraphicsItemGroup):
#Highlight the current staffline
if currentGuiTrack.staticExportItem["double"] and cursorExportObject["dotOnLine"] in (8, 10, 12, 14, 16):
lineNumber = int(cursorExportObject["dotOnLine"] / 2) + 1 #gui-stafflines are counted from 0 to n, top to bottom (listindex) while backend stafflines begin on -4
currentGuiTrack.staffLines[lineNumber].setGraphicsEffect(Cursor.stafflineEffect)
Cursor.stafflineEffect.setEnabled(True)
#currentGuiTrack.staffLines[lineNumber].setGraphicsEffect(Cursor.stafflineEffect)
#Cursor.stafflineEffect.setEnabled(True)
elif cursorExportObject["dotOnLine"] in (-4, -2, 0, 2, 4):
lineNumber = int(cursorExportObject["dotOnLine"] / 2) + 2 #gui-stafflines are counted from 0 to n, top to bottom (listindex) while backend stafflines begin on -4
currentGuiTrack.staffLines[lineNumber].setGraphicsEffect(Cursor.stafflineEffect)
Cursor.stafflineEffect.setEnabled(True)
else:
Cursor.stafflineEffect.setEnabled(False)
#currentGuiTrack.staffLines[lineNumber].setGraphicsEffect(Cursor.stafflineEffect)
#Cursor.stafflineEffect.setEnabled(True)
#else:
# Cursor.stafflineEffect.setEnabled(False)
class Playhead(QtWidgets.QGraphicsLineItem):
def __init__(self, parentScoreScene):
@ -129,12 +129,14 @@ class Playhead(QtWidgets.QGraphicsLineItem):
p = QtGui.QPen()
p.setColor(QtGui.QColor("red"))
p.setCosmetic(True)
p.setWidth(3)
self.setPen(p)
self.setAcceptHoverEvents(True)
#self.setAcceptHoverEvents(True)
api.callbacks.setPlaybackTicks.append(self.setCursorPosition)
api.callbacks.tracksChanged.append(self.setLineToWindowHeigth) #for new tracks
api.callbacks.updateTempoTrack.append(self.setLineToWindowHeigth)
self.setFlags(QtWidgets.QGraphicsItem.ItemIsMovable)
self.setCursor(QtCore.Qt.SizeHorCursor)
self.setAcceptedMouseButtons(QtCore.Qt.LeftButton)
self.setZValue(90)
#self.parentScoreScene.parentView.verticalScrollBar().valueChanged.connect(self.setLineToWindowHeigth)
@ -192,13 +194,13 @@ class Playhead(QtWidgets.QGraphicsLineItem):
p = 0
api.seek(p)
def hoverEnterEvent(self, event):
self.setCursor(QtCore.Qt.SizeHorCursor)
self.update() #the default implementation calls this. event.accept/ignore has no effect.
#def hoverEnterEvent(self, event):
# self.setCursor(QtCore.Qt.SizeHorCursor)
# self.update() #the default implementation calls this. event.accept/ignore has no effect.
def hoverLeaveEvent(self, event):
self.unsetCursor()
self.update() #the default implementation calls this. event.accept/ignore has no effect.
#def hoverLeaveEvent(self, event):
# self.unsetCursor()
# self.update() #the default implementation calls this. event.accept/ignore has no effect.
class Selection(QtWidgets.QGraphicsRectItem):
"""A semi-transparent rectangle that shows the current selection"""

16
qtgui/designer/mainwindow.py

@ -2,7 +2,7 @@
# Form implementation generated from reading ui file 'mainwindow.ui'
#
# Created by: PyQt5 UI code generator 5.13.0
# Created by: PyQt5 UI code generator 5.13.1
#
# WARNING! All changes made in this file will be lost!
@ -445,6 +445,10 @@ class Ui_MainWindow(object):
self.actionLyFree_Instruction.setObjectName("actionLyFree_Instruction")
self.actionLyRepeat = QtWidgets.QAction(MainWindow)
self.actionLyRepeat.setObjectName("actionLyRepeat")
self.actionZoom_In_Score_View = QtWidgets.QAction(MainWindow)
self.actionZoom_In_Score_View.setObjectName("actionZoom_In_Score_View")
self.actionZoom_Out_Score_View = QtWidgets.QAction(MainWindow)
self.actionZoom_Out_Score_View.setObjectName("actionZoom_Out_Score_View")
self.menuObjects.addAction(self.actionMetrical_Instruction)
self.menuObjects.addAction(self.actionClef)
self.menuObjects.addAction(self.actionKey_Signature)
@ -576,6 +580,8 @@ class Ui_MainWindow(object):
self.menuView.addAction(self.actionProperties)
self.menuView.addAction(self.actionWiden_Score_View)
self.menuView.addAction(self.actionShrink_Score_View)
self.menuView.addAction(self.actionZoom_In_Score_View)
self.menuView.addAction(self.actionZoom_Out_Score_View)
self.menuView.addSeparator()
self.menuView.addAction(self.actionPlayPause)
self.menuView.addAction(self.actionPlayFromEditCursor)
@ -886,9 +892,9 @@ class Ui_MainWindow(object):
self.actionDurationModLess.setShortcut(_translate("MainWindow", "Alt+Shift+Left"))
self.actionReset_Velocity_Duration_Mod.setText(_translate("MainWindow", "&Reset Velocity / Duration Mod."))
self.actionWiden_Score_View.setText(_translate("MainWindow", "&Widen Score View"))
self.actionWiden_Score_View.setShortcut(_translate("MainWindow", "Ctrl++"))
self.actionWiden_Score_View.setShortcut(_translate("MainWindow", "Ctrl+Shift++"))
self.actionShrink_Score_View.setText(_translate("MainWindow", "Shr&ink Score View"))
self.actionShrink_Score_View.setShortcut(_translate("MainWindow", "Ctrl+-"))
self.actionShrink_Score_View.setShortcut(_translate("MainWindow", "Ctrl+Shift+-"))
self.actionData_Editor.setText(_translate("MainWindow", "&Track Editor"))
self.actionData_Editor.setShortcut(_translate("MainWindow", "Ctrl+T"))
self.actionClef.setText(_translate("MainWindow", "&Clef"))
@ -968,3 +974,7 @@ class Ui_MainWindow(object):
self.actionLyBarline.setText(_translate("MainWindow", "Barline"))
self.actionLyFree_Instruction.setText(_translate("MainWindow", "Free Instruction"))
self.actionLyRepeat.setText(_translate("MainWindow", "Repeat"))
self.actionZoom_In_Score_View.setText(_translate("MainWindow", "Zoom In Score View"))
self.actionZoom_In_Score_View.setShortcut(_translate("MainWindow", "Ctrl++"))
self.actionZoom_Out_Score_View.setText(_translate("MainWindow", "Zoom Out Score View"))
self.actionZoom_Out_Score_View.setShortcut(_translate("MainWindow", "Ctrl+-"))

22
qtgui/designer/mainwindow.ui

@ -220,6 +220,8 @@
<addaction name="actionProperties"/>
<addaction name="actionWiden_Score_View"/>
<addaction name="actionShrink_Score_View"/>
<addaction name="actionZoom_In_Score_View"/>
<addaction name="actionZoom_Out_Score_View"/>
<addaction name="separator"/>
<addaction name="actionPlayPause"/>
<addaction name="actionPlayFromEditCursor"/>
@ -1311,7 +1313,7 @@
<string>&amp;Widen Score View</string>
</property>
<property name="shortcut">
<string>Ctrl++</string>
<string>Ctrl+Shift++</string>
</property>
</action>
<action name="actionShrink_Score_View">
@ -1319,7 +1321,7 @@
<string>Shr&amp;ink Score View</string>
</property>
<property name="shortcut">
<string>Ctrl+-</string>
<string>Ctrl+Shift+-</string>
</property>
</action>
<action name="actionData_Editor">
@ -1688,6 +1690,22 @@
<string>Repeat</string>
</property>
</action>
<action name="actionZoom_In_Score_View">
<property name="text">
<string>Zoom In Score View</string>
</property>
<property name="shortcut">
<string>Ctrl++</string>
</property>
</action>
<action name="actionZoom_Out_Score_View">
<property name="text">
<string>Zoom Out Score View</string>
</property>
<property name="shortcut">
<string>Ctrl+-</string>
</property>
</action>
</widget>
<resources/>
<connections/>

256
qtgui/grid.py

@ -0,0 +1,256 @@
#! /usr/bin/env python3
# -*- coding: utf-8 -*-
"""
Copyright 2019, Nils Hilbricht, Germany ( https://www.hilbricht.net )
This file is part of the Laborejo Software Suite ( https://www.laborejo.org ),
more specifically its template base application.
Laborejo2 is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
"""
#Standar Library
#Third Party
from PyQt5 import QtCore, QtGui, QtWidgets
#Our Template Modules
#Client Modules
from .constantsAndConfigs import constantsAndConfigs
class GuiGrid(QtWidgets.QGraphicsItemGroup):
"""The grid consists of vertical and horizontal lines.
Horiontal lines help to eyeball what pitch the cursor is on, vertical help to eyeball
the rhythm or tick positions e.g. when trying to find the right place for a tempo change or CC.
Horizontal lines have always a more-than-enough width and X position.
Vertical lines are always at the top of the screen and reach down more-than-enough.
We only need to take care that there are enough lines, not about their dimensions or positions.
Never is a line deleted, only new lines are added if new tracks are added or the musics overall
duration increases.
Also GraphicsView resize event and zooming out adds new lines.
A complete clean and redraw only happens when the tick rhythm changes.
"""
def __init__(self, parent, horizontalSize=None):
super(GuiGrid, self).__init__()
self.parent = parent #QGraphicsScene
self.setAcceptedMouseButtons(QtCore.Qt.NoButton) #each line has this set as well
self.setEnabled(False)
self.initialGridExists = False
self.gapVertical = None #gets recalculated if gridRhythm is changed by the user, or through stretching.
self.gapHorizontal = None #is constant, but for symetry reasons this is put into redrawTickGrid as well
self.width = None #recalculated in reactToresizeEventOrZoom
self.height = None #recalculated in reactToresizeEventOrZoom
self.reactToresizeEventOrZoom()
self.linesHorizontal = [] #later the grid lines will be stored here
self.linesVertical = [] #later the grid lines will be stored here
self.horizontalScrollbarWaitForGapJump = 0
self.oldHorizontalValue = 0
self.verticalScrollbarWaitForGapJump = 0
self.oldVerticalValue = 0
self.parent.parentView.verticalScrollBar().valueChanged.connect(self.reactToVerticalScroll)
self.parent.parentView.horizontalScrollBar().valueChanged.connect(self.reactToHorizontalScroll)
self.setOpacity(constantsAndConfigs.gridOpacity)
gridPen = QtGui.QPen(QtCore.Qt.DotLine)
gridPen.setCosmetic(True)
def reactToHorizontalScroll(self, value):
if not self.initialGridExists:
return
#Keep horizontal lines in view
leftBorderAsScenePos = self.parent.parentView.mapToScene(0, 0).x()
for hline in self.linesHorizontal:
hline.setX(leftBorderAsScenePos)
#Shift vertical lines to new positions, respecting the grid steps
delta = value - self.oldHorizontalValue #in pixel. positive=right, negative=left. the higher the number the faster the scrolling.
delta *= 1/constantsAndConfigs.zoomFactor
self.horizontalScrollbarWaitForGapJump += delta
self.oldHorizontalValue = value
if abs(self.horizontalScrollbarWaitForGapJump) > self.gapVertical: #collect scrollpixels until we scrolled more than one gap
gapMultiplier, rest = divmod(self.horizontalScrollbarWaitForGapJump, self.gapVertical) #really fast scrolling can jump more than one gap
for vline in self.linesVertical:
vline.setX(vline.x() + gapMultiplier*self.gapVertical)
self.horizontalScrollbarWaitForGapJump = rest #keep the rest for the next scroll
assert abs(self.horizontalScrollbarWaitForGapJump) < self.gapVertical
def reactToVerticalScroll(self, value):
if not self.initialGridExists:
return
#Keep vertical lines in view
topBorderAsScenePos = self.parent.parentView.mapToScene(0, 0).y()
for vline in self.linesVertical:
vline.setY(topBorderAsScenePos)
#Shift horizontal lines to a new position, respecting the staffline gap
delta = value - self.oldVerticalValue #in pixel. positive=down, negative=up. the higher the number the faster the scrolling.
delta *= 1/constantsAndConfigs.zoomFactor
self.verticalScrollbarWaitForGapJump += delta
self.oldVerticalValue = value
if abs(self.verticalScrollbarWaitForGapJump) > self.gapHorizontal: #collect scrollpixels until we scrolled more than one gap
gapMultiplier, rest = divmod(self.verticalScrollbarWaitForGapJump, self.gapHorizontal) #really fast scrolling can jump more than one gap
for hline in self.linesHorizontal:
hline.setY(hline.y() + gapMultiplier*self.gapHorizontal)
self.verticalScrollbarWaitForGapJump = rest #keep the rest for the next scroll
assert abs(self.verticalScrollbarWaitForGapJump) < self.gapHorizontal
def reactToresizeEventOrZoom(self):
"""Called by the Views resizeEvent.
When the views geometry changes or zooms we may need to create more lines.
Never delete and never make smaller though."""
scoreViewWidgetHeight = self.parent.parentView.height()
scoreViewWidgetWidth = self.parent.parentView.width()
if (not self.height) or scoreViewWidgetHeight * 1.1 > self.height:
self.height = scoreViewWidgetHeight * 1.1
if (not self.width) or scoreViewWidgetWidth * 1.1 > self.width:
self.width = scoreViewWidgetWidth * 1.1
if self.initialGridExists:
assert self.linesHorizontal and self.linesVertical, (self.linesHorizontal, self.linesVertical)
assert self.parent.parentView.isVisible(), self.parent.parentView.isVisible()
self._fillVerticalLines()
self._fillHorizontalLines()
#else:
#reactToresizeEventOrZoom without grid
#self.redrawTickGrid() #this happens only once, on program start. Afterwards it's user triggered by changing the grid rhythm
def _fillVerticalLines(self):
"""Only allowed to get called by reactToresizeEventOrZoom because we need an updated
self.height value"""
#Check if we need longer lines. Do this before creating new lines, because new lines
#have the heighest
oldHeight = self.linesVertical[-1].line().y2() #this is a bit less than .length() so we use that
if self.height > oldHeight:
for vline in self.linesVertical:
line = vline.line()
line.setLength(self.height)
vline.setLine(line)
#Check if we need new lines
newLineCount = int(self.width / self.gapVertical)
if newLineCount > self.nrOfVerticalLines:
newLineCount += int(newLineCount - self.nrOfVerticalLines)
self._createVerticalLines(start=self.nrOfVerticalLines, end=newLineCount) #This draws only yet nonexisting lines.
self.nrOfVerticalLines = newLineCount #for next time
def _fillHorizontalLines(self):
"""Only allowed to get called by reactToresizeEventOrZoom because we need an updated
self.width value.
To be honest.. this is also called by stretchXCoordinates when shrinking the display so
we need more lines. That does not depend on self.width but on the newLineCount """
#Check if we need longer lines. Do this before creating new lines, because new lines
#have the heighest
oldWidth = self.linesHorizontal[-1].line().x2() #this is a bit less than .length() so we use that
if self.width > oldWidth:
for hline in self.linesHorizontal:
line = hline.line()
line.setLength(self.width)
hline.setLine(line)
#Check if we need new lines
newLineCount = int(self.height / self.gapHorizontal)
if newLineCount > self.nrOfHorizontalLines:
newLineCount += int(newLineCount - self.nrOfHorizontalLines)
self._createHorizontalLines(start=self.nrOfHorizontalLines, end=newLineCount) #This draws only yet nonexisting lines.
self.nrOfHorizontalLines = newLineCount #for next time
def stretchXCoordinates(self, factor):
"""Reposition the items on the X axis.
Call goes through all parents/children, starting from ScoreView._stretchXCoordinates.
Docstring there."""
self.gapVertical = constantsAndConfigs.gridRhythm / constantsAndConfigs.ticksToPixelRatio #this is necessarry every time after stretching (ticksToPixelRatio) and after changing the gridRhythm
for vline in self.linesVertical:
vline.setX(vline.x() * factor)
self._fillVerticalLines()
#for hline in self.linesHorizontal:
# stretchLine(hline, factor)
def updateMode(self, nameAsString):
assert nameAsString in constantsAndConfigs.availableEditModes
if nameAsString == "block":
for l in self.linesVertical:
l.hide()
else:
for l in self.linesVertical:
l.show()
def _createVerticalLines(self, start, end):
"""Lines get an offset that matches the grid-rythm.
So we get more draw area left, if the sceneRect will move into that area for some reason"""
for i in range(start, end): #range includes the first value but not the end. We know that and all functions in GuiGrid are designed accordingly. For example reactToLongerDuration starts on the highest value of the old number of lines since that was never drawn in the last round.
vline = self.parent.addLine(0, -5*self.gapHorizontal, 0, self.height, GuiGrid.gridPen) #x1, y1, x2, y2, pen
self.addToGroup(vline) #first add to group, then set pos
vline.setPos(i * self.gapVertical, 0)
vline.setEnabled(False)
vline.setAcceptedMouseButtons(QtCore.Qt.NoButton)
self.linesVertical.append(vline)
def _createHorizontalLines(self, start, end):
"""Lines get an offset that matches the pitch and staff-lines
So we get more draw area on top, if the sceneRect will move into that area for some reason"""
for i in range(start, end): #range includes the first value but not the end. We know that and all functions in GuiGrid are designed accordingly. For example reactToMoreTracks starts on the highest value of the old number of lines since that was never drawn in the last round.
hline = self.parent.addLine(0, 0, self.width, 0, GuiGrid.gridPen) #x1, y1, x2, y2, pen
self.addToGroup(hline) #first add to group, then set pos
hline.setPos(0, i * self.gapHorizontal)
hline.setEnabled(False)
hline.setAcceptedMouseButtons(QtCore.Qt.NoButton)
self.linesHorizontal.append(hline)
def redrawTickGrid(self):
"""A complete redraw.
This gets called once after the main window gets shown. (in init main window).
After that only called after the used changes the tick rhythm manually.
"""
if self.initialGridExists:
#it is possible that the grid did not change. proceed nevertheless
for l in self.linesHorizontal + self.linesVertical:
self.parent.removeWhenIdle(l)
self.linesHorizontal = [] #like the staff lines
self.linesVertical = [] #makes comparing tick positions easier by eye
self.gapVertical = constantsAndConfigs.gridRhythm / constantsAndConfigs.ticksToPixelRatio #this is necessarry every time after stretching (ticksToPixelRatio) and after changing the gridRhythm
self.gapHorizontal = constantsAndConfigs.stafflineGap #never changes
self.nrOfHorizontalLines = int(self.height / self.gapHorizontal) #we save that value if the score grows and we need more lines. Initially we use the screen size, not the content.
self.nrOfVerticalLines = int(self.width / self.gapVertical) #we save that value if the score grows and we need more lines. Initially we use the screen size, not the content.
self._createVerticalLines(0, self.nrOfVerticalLines)
self._createHorizontalLines(0, self.nrOfHorizontalLines)
self.initialGridExists = True

2
qtgui/items.py

@ -513,7 +513,7 @@ class GuiChord(GuiItem):
for noteExportObject in self.staticItem["notelist"]:
#notes in a chord come in the order highest to lowest
#determine if we have two neighbouring noteheads. If yes we shift one of the heads to the right
assert lastDotOnLine > noteExportObject["dotOnLine"]
assert lastDotOnLine >= noteExportObject["dotOnLine"]
if lastDotOnLine - noteExportObject["dotOnLine"] == 1:
moveThisNoteheadToTheRight = True
lastDotOnLine = noteExportObject["dotOnLine"]

11
qtgui/mainwindow.py

@ -39,7 +39,6 @@ from midiinput.stepmidiinput import stepMidiInput #singleton instance
from .constantsAndConfigs import constantsAndConfigs
from .menu import MenuActionDatabase
from .scoreview import ScoreView
from .structures import GuiScore
from .trackEditor import TrackEditor
from .resources import *
@ -63,7 +62,7 @@ class MainWindow(TemplateMainWindow):
About.didYouKnow = [
QtCore.QCoreApplication.translate("About", "<p>Most commands work in the appending position (last position in a track) and apply to the item before it.</p><p>Use it to apply dots, sharps and flats on the item you just inserted without moving the cursor back and forth.</p>"),
QtCore.QCoreApplication.translate("About", "<p>Learn the keyboard shortcuts! Laborejo is designed to work with the keyboard alone and with midi instruments for full speed.</p>Everytime you grab your mouse you loose concentration, precision and time."),
QtCore.QCoreApplication.translate("About", "<p>Spread/shrink the space between notes with Ctrl+Mousewheel or Ctrl with Plus and Minus.</p><p>Full zoom by additionaly holding the Shift key.</p>"),
QtCore.QCoreApplication.translate("About", "<p>Spread/shrink the space between notes with Ctrl+Shift+Mousewheel or Ctrl+Shift with Plus and Minus.</p>"),
QtCore.QCoreApplication.translate("About", "<p>Click with the left mouse button to set the cursor to that position. Hold Shift to create a selection.</p>"),
QtCore.QCoreApplication.translate("About", "<p>Most commands can be applied to single notes and selections equally.</p><p>Use Shift + movement-keys to create selections.</p>"),
QtCore.QCoreApplication.translate("About", "<p>Blocks and Tracks can be moved in Block-View Mode [F6]. Use Shift+Middle Mouse Button to move blocks and Alt+Middle to reorder tracks.</p>"),
@ -124,6 +123,12 @@ class MainWindow(TemplateMainWindow):
self.scoreView.scoreScene.grid.redrawTickGrid() #Init the grid only after everything got loaded and drawn to prevent a gap in the display. #TODO: which might be a bug. but this here works fine.
def zoom(self, scaleFactor:float):
"""Scale factor is absolute"""
self.scoreView.zoom(scaleFactor)
def stretchXCoordinates(self, factor:float):
self.scoreView.stretchXCoordinates(factor)
def updateStatusBar(self, exportCursorDict):
"""Every cursor movement updates the statusBar message"""
@ -134,7 +139,7 @@ class MainWindow(TemplateMainWindow):
print (c)
if i:
ly = i.lilypond()
if (not ly) or len(ly) > 10:
if (not ly) or len(ly) > 13:
ly = ""
else:
ly = "Lilypond: <b>{}</b>".format(ly.replace("<", "&#60;").replace(">", "&#62;"))

8
qtgui/menu.py

@ -190,10 +190,10 @@ class MenuActionDatabase(object):
self.mainWindow.ui.actionDelete_Current_Track : api.deleteCurrentTrack,
self.mainWindow.ui.actionUse_Current_Track_as_Metronome : api.useCurrentTrackAsMetronome,
self.mainWindow.ui.actionMidi_In_is_Active : self.toggleMidiInIsActive,
#self.mainWindow.ui.actionZoom_In_Score : self.mainWindow.scoreView.zoomIn,
#self.mainWindow.ui.actionZoom_Out_Score : self.mainWindow.scoreView.zoomOut,
self.mainWindow.ui.actionWiden_Score_View : self.mainWindow.scoreView.widen,
self.mainWindow.ui.actionShrink_Score_View : self.mainWindow.scoreView.shrinken,
self.mainWindow.ui.actionZoom_In_Score_View : self.mainWindow.zoomIn,
self.mainWindow.ui.actionZoom_Out_Score_View : self.mainWindow.zoomOut,
self.mainWindow.ui.actionWiden_Score_View : self.mainWindow.widen,
self.mainWindow.ui.actionShrink_Score_View : self.mainWindow.shrinken,
self.mainWindow.ui.actionSave : api.save,
self.mainWindow.ui.actionShow_PDF : api.showPDF,

488
qtgui/structures.py → qtgui/musicstructures.py

@ -22,26 +22,27 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
import logging; logging.info("import {}".format(__file__))
#Standard Library
from math import log
#Third party
from PyQt5 import QtCore, QtGui, QtWidgets
#Template
from template.qtgui.helper import stringToColor
from template.qtgui.helper import stretchLine, stretchRect, callContextMenu, removeInstancesFromScene
#Our own files
import engine.api as api
from . import graphs
from .items import staticItem2Item, GuiTieCurveGraphicsItem
from .constantsAndConfigs import constantsAndConfigs
from .cursor import Cursor, Playhead, Selection
from .conductor import Conductor, ConductorTransparentBlock
from .graphs import CCGraphTransparentBlock
from template.qtgui.helper import stretchLine, stretchRect, callContextMenu, removeInstancesFromScene
from template.qtgui.grid import GuiGrid
from .submenus import BlockPropertiesEdit
from . import graphs
from hashlib import md5 #string to color
import engine.api as api
oneRectToReturnThemAll = QtCore.QRectF(0,0,0,0) #prevent the annoying "NotImplementError" from Qt for boundingRect. #TODO: implement in each class for realsies
cosmeticPen = QtGui.QPen()
cosmeticPen.setCosmetic(True)
class GuiBlockHandle(QtWidgets.QGraphicsRectItem):
"""A simplified version of a Block. Since we don't use blocks in the GUI, only in the backend
we still need them sometimes as macro strutures, where we don't care about the content.
@ -53,6 +54,8 @@ class GuiBlockHandle(QtWidgets.QGraphicsRectItem):
"""
def __init__(self, parent, staticExportItem, x, y, w, h):
super().__init__(x, y, w, h)
self.setFlag(QtWidgets.QGraphicsItem.ItemHasNoContents, True) #only child items. Without this we get notImplementedError: QGraphicsItem.paint() is abstract and must be overridden
self.setFlag(QtWidgets.QGraphicsItem.ItemContainsChildrenInShape, True)
self.parent = parent #GuiTrack instance
self.color = None #inserted by the creating function in GuiTrack
self.trans = QtGui.QColor("transparent")
@ -185,6 +188,9 @@ class GuiTrack(QtWidgets.QGraphicsItem):
def __init__(self, parentScore, staticExportItem):
super().__init__()
self.setFlag(QtWidgets.QGraphicsItem.ItemHasNoContents, True) #only child items. Without this we get notImplementedError: QGraphicsItem.paint() is abstract and must be overridden
self.setFlag(QtWidgets.QGraphicsItem.ItemContainsChildrenInShape, True)
self.parentScore = parentScore
self.staticExportItem = staticExportItem #This is not the notes but the track meta data. The notes are called staticRepresentationList
self.items = [] #this is used for stretching and processing of the current items. scene clear is done diffently. See self.createGraphicItemsFromData
@ -236,12 +242,6 @@ class GuiTrack(QtWidgets.QGraphicsItem):
callContextMenu(listOfLabelsAndFunctions)
event.accept()
def paint(self, *args):
pass
def boundingRect(self, *args):
return oneRectToReturnThemAll
def secondStageInitNowThatWeHaveAScene(self):
"""ScoreScene.redraw() calls this after the track was inserted into the scene and therefore
has a position, opacity, parent item etc. (All of that is not read in normal __init__)"""
@ -275,7 +275,7 @@ class GuiTrack(QtWidgets.QGraphicsItem):
bgItem = QtWidgets.QGraphicsRectItem(0, 0, block["completeDuration"] / constantsAndConfigs.ticksToPixelRatio, h) #x, y, w, h
bgItem.setFlag(QtWidgets.QGraphicsItem.ItemIgnoresParentOpacity)
bgItem.setPen(QtGui.QColor("transparent"))
color = self.stringToColor(block["name"])
color = stringToColor(block["name"])
bgItem.setBrush(color)
bgItem.setParentItem(self)
self.backgroundBlockColors.append(bgItem)
@ -392,14 +392,13 @@ class GuiTrack(QtWidgets.QGraphicsItem):
class TrackAnchor(QtWidgets.QGraphicsItem):
"""Handling all items as individuals when deleting a track to redraw it is too much.
Better let Qt handle it all at once."""
oneRectToReturnThemAll = QtCore.QRectF(0,0,0,0) #prevent the annoying "NotImplementError" from Qt for boundingRect. For items that don't need any collision detection.
def __init__(self, parent):
super().__init__()
self.parent = parent
def paint(self, *args):
pass
def boundingRect(self, *args):
return oneRectToReturnThemAll
self.setFlag(QtWidgets.QGraphicsItem.ItemHasNoContents, True) #only child items. Without this we get notImplementedError: QGraphicsItem.paint() is abstract and must be overridden
self.setFlag(QtWidgets.QGraphicsItem.ItemContainsChildrenInShape, True)
def createGraphicItemsFromData(self, staticRepresentationList):
"""Create staff objects including simple barlines"""
@ -476,15 +475,6 @@ class GuiTrack(QtWidgets.QGraphicsItem):
return th
return None #After the last block.
def stringToColor(self, st):
"""Convert a string to QColor. Same string, same color
Is used for track coloring"""
if st:
c = md5(st.encode()).hexdigest()
return QtGui.QColor(int(c[0:9],16) % 255, int(c[10:19],16) % 255, int(c[20:29],16)% 255, 255)
else:
return QtGui.QColor(255,255,255,255) #Return White
def updateMode(self, nameAsString):
"""Modes are opacity based, not show and hide.
This gives us the option that children can ignore the opacity (via qt-flag).
@ -522,440 +512,4 @@ class GuiTrack(QtWidgets.QGraphicsItem):
for tbh in self.transparentBlockHandles:
tbh.blockMode()
class GuiScore(QtWidgets.QGraphicsScene):
def __init__(self, parentView):
super().__init__()
self.parentView = parentView
self.setItemIndexMethod(QtWidgets.QGraphicsScene.NoIndex)
self.tracks = {} #trackId:guiTrack, #if we don't save the instances here in Python space Qt will loose them and they will not be displayed without any error message.
self.deleteOnIdleStack = [] # a stack that holds hidden items that need to be deleted. Hiding is much faster than deleting so we use that for the blocking function. Since we always recreate items and never re-use this is ok as a list. no need for a set.
self._deleteOnIdleLoop = QtCore.QTimer()
self._deleteOnIdleLoop.start(0) #0 means "if there is time"
self._deleteOnIdleLoop.timeout.connect(self._deleteOnIdle) #processes deleteOnIdleStack
self.duringTrackDragAndDrop = None #switched to a QGraphicsItem (e.g. GuiTrack) while a track is moved around by the mouse
self.duringBlockDragAndDrop = None #switched to a QGraphicsItem (e.g. GuiTrack) while a block is moved around by the mouse
self.conductor = Conductor(parentView = self.parentView)
self.addItem(self.conductor)
self.conductor.setPos(0, -1 * self.conductor.totalHeight)
self.yStart = self.conductor.y() - self.conductor.totalHeight/2
self.hiddenTrackCounter = QtWidgets.QGraphicsSimpleTextItem("") #filled in by self.redraw on callback tracksChanged (loading or toggling visibility of backend tracks)
self.addItem(self.hiddenTrackCounter)
self.backColor = QtGui.QColor()
self.backColor.setNamedColor("#fdfdff")
self.setBackgroundBrush(self.backColor)
self.grid = GuiGrid(parent=self)
self.addItem(self.grid)
self.grid.setPos(0, -20 * constantsAndConfigs.stafflineGap) #this is more calculation than simply using self.yStart, and might require manual adjustment in the future, but at least it guarantees the grid matches the staffline positions
self.grid.setZValue(-50)
self.cachedSceneHeight = 0 #set in self.redraw. Used by updateTrack to set the sceneRect
#All Cursors
self.cursor = Cursor()
self.addItem(self.cursor)
self.selection = Selection()
self.addItem(self.selection)
self.playhead = Playhead(self)
self.addItem(self.playhead)
self.playhead.setY(self.yStart)
#Callbacks
api.callbacks.tracksChanged.append(self.redraw)
api.callbacks.updateTrack.append(self.updateTrack)
api.callbacks.updateBlockTrack.append(self.trackPaintBlockBackgroundColors)
api.callbacks.updateGraphTrackCC.append(self.updateGraphTrackCC)
api.callbacks.updateGraphBlockTrack.append(self.updateGraphBlockTrack)
api.callbacks.graphCCTracksChanged.append(self.syncCCsToBackend)
def updateMode(self, nameAsString):
assert nameAsString in constantsAndConfigs.availableEditModes
for track in self.tracks.values():
track.updateMode(nameAsString)
self.grid.updateMode(nameAsString)
def maxTrackLength(self):
if self.tracks:
return max(tr.lengthInPixel for tr in self.tracks.values())
#return max(max(tr.lengthInPixel for tr in self.tracks.values()), self.parentView.geometry().width())
#return max(max(tr.lengthInPixel for tr in self.scene().tracks.values()), self.scene().parentView.geometry().width()) + self.scene().parentView.geometry().width()
else:
return 0 #self.parentView.geometry().width()
def updateSceneRect(self):
self.parentView.setSceneRect(QtCore.QRectF(-5, self.yStart, self.maxTrackLength() + 300, self.cachedSceneHeight)) #x,y,w,h
def updateTrack(self, trackId, staticRepresentationList):
"""for callbacks"""
if trackId in self.tracks:
self.tracks[trackId].redraw(staticRepresentationList)
#else:
#hidden track. But this can still happen through the data editor
self.parentView.updateMode()
self.updateSceneRect()
def trackPaintBlockBackgroundColors(self, trackId, staticBlocksRepresentation):
if trackId in self.tracks:
self.tracks[trackId].paintBlockBackgroundColors(staticBlocksRepresentation)
#else:
#hidden track.
def updateGraphTrackCC(self, trackId, ccNumber, staticRepresentationList):
"""TrackId is a real notation track which has a dict of CCs"""
if trackId in self.tracks:
track = self.tracks[trackId]
ccPath = track.ccPaths[ccNumber]
ccPath.createGraphicItemsFromData(staticRepresentationList)
def updateGraphBlockTrack(self, trackId, ccNumber, staticRepresentationList):
"""TrackId is a real notation track which has a dict of CCs"""
if trackId in self.tracks:
self.tracks[trackId].ccPaths[ccNumber].updateGraphBlockTrack(staticRepresentationList)
def toggleNoteheadsRectangles(self):
for track in self.tracks.values():
track.toggleNoteheadsRectangles()
def syncCCsToBackend(self, trackId, listOfCCsInThisTrack):
"""Delete ccGraphs that are gui-only,
create gui versions of graphs that are backend-only.
Don't touch the others.
This is the entry point for new CCPaths. They get created
only here.
"""
#TODO: and moved from one CC value to another?
guiCCs = set(self.tracks[trackId].ccPaths.keys())
for backendCC in listOfCCsInThisTrack:
if backendCC in guiCCs:
guiCCs.remove(backendCC) #all right. no update needed.
else: #new CC. not existent in the Gui yet. Create.
#This is the place where we create new CCPaths
new = graphs.CCPath(parentGuiTrack = self.tracks[trackId], parentDataTrackId = trackId)
self.tracks[trackId].ccPaths[backendCC] = new #store in the GUI Track
new.setParentItem(self.tracks[trackId])
new.setPos(0,0)
#new.setZValue(100)
#all items left in the set are GUI-only CCs. which means the backend graphs were deleted. Delete them here as well.
for cc in guiCCs:
self.removeWhenIdle(self.tracks[trackId].ccPaths[cc])
del self.tracks[trackId].ccPaths[cc]
def redraw(self, listOfStaticTrackRepresentations):
"""The order of guiTracks depends on the backend index.
This way it is a no-brainer, we don't need to maintain our own
order, just sync with the backend when the callback comes in.
Also handles meta data like track names.
But not the actual track content, which is done
through self.updateTrack which has its own api-callback
called by callbacksDatabase.tracksChanged"""
for track in self.tracks.values():
track.hide()
doubleTrackOffset = 0
for trackExportObject in listOfStaticTrackRepresentations:
if not trackExportObject["id"] in self.tracks:
guiTrack = GuiTrack(self, trackExportObject)
self.tracks[trackExportObject["id"]] = guiTrack
self.addItem(guiTrack)
guiTrack.secondStageInitNowThatWeHaveAScene()
self.tracks[trackExportObject["id"]].staticExportItem = trackExportObject
self.tracks[trackExportObject["id"]].setPos(0, constantsAndConfigs.trackHeight * trackExportObject["index"] + doubleTrackOffset)
self.tracks[trackExportObject["id"]].setZValue(0) #direct comparison only possible with the grid, which is at -50
self.tracks[trackExportObject["id"]].nameGraphic.setText(trackExportObject["name"])
self.tracks[trackExportObject["id"]].show()
if trackExportObject["double"]:
doubleTrackOffset += constantsAndConfigs.trackHeight
toDelete = []
for trackId, track in self.tracks.items():
if not track.isVisible():
toDelete.append((trackId, track))
for trackId_, track_ in toDelete:
self.removeWhenIdle(track_)
del self.tracks[trackId_]
#Finally, under the last track, tell the user how many hidden tracks there are
nrOfHiddenTracks = len(api.session.data.hiddenTracks)
if nrOfHiddenTracks:
self.hiddenTrackCounter.setText("… and {} hidden tracks".format(nrOfHiddenTracks))
else: #remove previous status message
self.hiddenTrackCounter.setText("")
belowLastTrack = constantsAndConfigs.trackHeight * (trackExportObject["index"] + 1) + doubleTrackOffset
self.hiddenTrackCounter.setPos(5, belowLastTrack)
self.cachedSceneHeight = belowLastTrack + constantsAndConfigs.trackHeight
def removeWhenIdle(self, item):
"""Call this function instead of removeItem. You are responsible to delete the item from any
list or other container where it was stored in the meantime yourself.
Other methods tried:
-removeItem much slower
-create second scene and do scene2.addItem to get it out here - same as removeItem, if not slower
-delete a track and recreate a new one - same as all removeItems
-just hide the track and create a new one. slightly slower as hiding just the items.
-item.setFlag(QtWidgets.QGraphicsItem.ItemHasNoContents) slow, but not as slow as removeItem
-just hide items, never delete them is fast but gets slower the more items are hidden in the track
"""
item.hide()
self.deleteOnIdleStack.append(item) #This will actually delete the item and remove it from the scene when there is idle time
def _deleteOnIdle(self):
"""Hiding a QGraphicsItem is much faster than removing it from the scene. To keep the
GUI responsive but also to get rid of the old data we hide in createGraphicItemsFromData
and whenever there is time the actual removing and deleting will happen here.
This function is connected to a timer(0) defined in self init.
"""
if self.deleteOnIdleStack:
deleteMe = self.deleteOnIdleStack.pop()
self.removeItem(deleteMe) #This is the only line in the program that should call scene.removeItem
del deleteMe
def trackAt(self, qScenePosition):
"""trackAt always returns the full GuiTrack, even if in ccEdit mode."""
if qScenePosition.y() < self.conductor.y() + self.conductor.totalHeight/4:
return self.conductor
for guiTrack in sorted(self.tracks.values(), key = lambda gT: gT.y()):
if guiTrack.y() >= qScenePosition.y() - constantsAndConfigs.trackHeight/2:
return guiTrack
else:
return None #no track here.
def blockAt(self, qScenePosition):
track = self.trackAt(qScenePosition)
if track is self.conductor:
return self.conductor.blockAt(qScenePosition.x())
if self.parentView.mode() in ("block", "notation"):
if track:
return track.blockAt(qScenePosition.x())
elif self.parentView.mode() == "cc":
if track and constantsAndConfigs.ccViewValue in track.ccPaths:
return track.ccPaths[constantsAndConfigs.ccViewValue].blockAt(qScenePosition.x())
else:
raise NotImplementedError
return None
def stretchXCoordinates(self, factor):
"""Reposition the items on the X axis.
Call goes through all parents/children, starting from ScoreView._stretchXCoordinates.
Docstring there."""
#The big structures have a fixed position at (0,0) and move its child items, like notes, internally
#Some items, like the cursor, move around, as a whole item, in the scene directly and need no stretchXCoordinates() themselves.
self.grid.stretchXCoordinates(factor)
self.conductor.stretchXCoordinates(factor)
self.cursor.setX(self.cursor.pos().x() * factor)
self.playhead.setX(self.playhead.pos().x() * factor)
for track in self.tracks.values():
track.stretchXCoordinates(factor)
self.updateSceneRect()
#Macro-Structure: Score / Track / Block Moving and Duplicating
#Hold the ALT 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):
#Triggers only if there is no shortcut for an action.
modifiers = QtWidgets.QApplication.keyboardModifiers()
if modifiers == QtCore.Qt.AltModifier:
Alt
super().keyPressEvent(event)
"""
def mousePressEvent(self, event):
"""Pressing the mouse button is the first action of drag
and drop. We make the mouse cursor invisible so the user
can see where the point is going
When in blockmode pressing the middle button combined with either Alt or Shift moves tracks and blocks.
"""
if event.button() == 4 and self.parentView.mode() in ("block", "cc"): # Middle Button
modifiers = QtWidgets.QApplication.keyboardModifiers()
if modifiers == QtCore.Qt.ShiftModifier: #block move
block = self.blockAt(event.scenePos())
if block: #works for note blocks and conductor blocks
block.staticExportItem["guiPosStart"] = block.pos() #without shame we hijack the backend-dict.
self.duringBlockDragAndDrop = block
block.mousePressEventCustom(event)
elif modifiers == QtCore.Qt.AltModifier and self.parentView.mode() == "block": #track move
track = self.trackAt(event.scenePos())
if track and not track is self.conductor:
self.parentView.setCursor(QtCore.Qt.BlankCursor)
self.cursor.hide()
track.staticExportItem["guiPosStart"] = track.pos()
self.duringTrackDragAndDrop = track
super().mousePressEvent(event)
def mouseMoveEvent(self, event):
"""Catches certain mouse events for moving tracks and blocks.
Otherwise the event is propagated to the real QGraphicsItem.
Don't forget that an item needs to have the flag movable or selectable or else
it will not get mouseRelease or mouseMove events. MousePress always works."""
if self.duringTrackDragAndDrop:
x = self.duringTrackDragAndDrop.staticExportItem["guiPosStart"].x()
y = event.scenePos().y()
self.duringTrackDragAndDrop.setPos(x, y)
elif self.duringBlockDragAndDrop:
self.duringBlockDragAndDrop.mouseMoveEventCustom(event)
super().mouseMoveEvent(event)
def mouseReleaseEvent(self, event):
"""Catches certain mouse events for moving tracks and blocks.
Otherwise the event is propagated to the real QGraphicsItem.
Don't forget that an item needs to have the flag movable or selectable or else
it will not get mouseRelease or mouseMove events. MousePress always works."""
self.parentView.unsetCursor() #While moving stuff the mouse-cursor is hidden. Reset.
#self.cursor.show() #Our own position cursor #TODO: why was that in here? It shows the cursor after a mouseclick, even in CC mode (it should not)
tempBlockDragAndDrop = self.duringBlockDragAndDrop
tempTrackDragAndDrop = self.duringTrackDragAndDrop
self.duringBlockDragAndDrop = None
self.duringTrackDragAndDrop = None
#Now we can exit the function safely at any time without the need to reset the dragAndDrop storage in different places
if tempTrackDragAndDrop: #This is only for GuiTrack aka note tracks. CC tracks follow the gui track and the conductor cannot be moved.
assert not tempBlockDragAndDrop
assert type(tempTrackDragAndDrop) is GuiTrack
dragTrackPosition = tempTrackDragAndDrop.pos().y()
trackPositions = sorted([guiTrack.pos().y() for id, guiTrack in self.tracks.items()])
trackPositions.remove(dragTrackPosition)
listOfTrackIds = api.session.data.asListOfTrackIds()
listOfTrackIds.remove(tempTrackDragAndDrop.staticExportItem["id"])
#Calculate the new track position and trigger a redraw
canditateForNewTrackPosition = 0 #this takes care of a score with only one track and also if you move a track to the 0th position
for y in trackPositions:
if dragTrackPosition >= y:
canditateForNewTrackPosition = trackPositions.index(y) +1
listOfTrackIds.insert(canditateForNewTrackPosition, tempTrackDragAndDrop.staticExportItem["id"])
if len(listOfTrackIds) == 1:
tempTrackDragAndDrop.setPos(tempTrackDragAndDrop.staticExportItem["guiPosStart"]) #no need to trigger an api call with undo history etc.
else:
api.rearrangeTracks(listOfTrackIds)
elif tempBlockDragAndDrop: #CC blocks, note blocks and conductor blocks
assert not tempTrackDragAndDrop
tempBlockDragAndDrop.mouseReleaseEventCustom(event)
targetTrack = self.trackAt(event.scenePos()) #this ALWAYS returns a Conductor, GuiTrack or None. Not a CC sub-track. for CC see below when we test the block type and change this variable.
targetBlock = self.blockAt(event.scenePos())
dragBlockId = tempBlockDragAndDrop.staticExportItem["id"]
#First some basic checks:
if not targetTrack: #Only drag and drop into tracks.
return None
if targetBlock is tempBlockDragAndDrop: #block got moved on itself
return None
#Now check what kind of block moving we are dealing with
#If the drag and drop mixes different block/track types we exit
blockType = type(tempBlockDragAndDrop)
conductorBlock = noteBlock = ccBlock = False
if blockType is GuiBlockHandle:
if type(targetTrack) is GuiTrack and self.parentView.mode() in ("block", "notation"):
noteBlock = True
else: #Drag and Drop between different track types.
return None
elif blockType is ConductorTransparentBlock:
if targetTrack is self.conductor:
conductorBlock = True
else: #Drag and Drop between different track types.
return None
elif blockType is CCGraphTransparentBlock:
if (not type(targetTrack) is GuiTrack) or (not self.parentView.mode() == "cc"):
return None
ccBlock = True
if constantsAndConfigs.ccViewValue in targetTrack.ccPaths: #this is only the backend database of CCs in tracks but it should be in sync.
targetTrackId = targetTrack.staticExportItem["id"] #we need this later. save it before changing the targetBlock variable
targetTrack = targetTrack.ccPaths[constantsAndConfigs.ccViewValue]
else:
return None #TODO: Create a new CC sub-track with the moved block as first block. Mainly a backend call. Needs a call at the end of this function as well.
else:
raise TypeError("Block must be a conductor, note or CC type but is {}".format(blockType))
#We now have a track and the track type matches the block type.
#Find the position to insert.
if targetBlock is None: #behind the last block
positionToInsert = len(targetTrack.transparentBlockHandles) #essentially we want append() but insert() is compatible with all types of operation here. len is based 1, so len results in "after the last one" position.
else:
assert type(targetBlock) is blockType
positionToInsert = targetTrack.transparentBlockHandles.index(targetBlock)
assert positionToInsert >= 0
#Create the new order by putting the old block into a new slot and removing it from its old position
newBlockOrder = [guiBlock.staticExportItem["id"] for guiBlock in targetTrack.transparentBlockHandles]
if targetTrack == tempBlockDragAndDrop.parent:
newBlockOrder.remove(dragBlockId) #block will be at another position and removed from its old one.
newBlockOrder.insert(positionToInsert, dragBlockId)
#Finally call the appropriate backend function which will trigger a GuiUpdate.
if targetTrack is tempBlockDragAndDrop.parent: #Same track or within the conductor track
if conductorBlock:
assert targetTrack is self.conductor
api.rearrangeTempoBlocks(newBlockOrder)
elif noteBlock:
api.rearrangeBlocks(targetTrack.staticExportItem["id"], newBlockOrder)
elif ccBlock:
api.rearrangeCCBlocks(targetTrackId, constantsAndConfigs.ccViewValue, newBlockOrder)
#else is already catched by a Else-TypeError above
else: #Different Track,
if conductorBlock or targetTrack is self.conductor:
raise RuntimeError("How did this slip through? Checking for cross-track incompatibility was already done above")
elif noteBlock:
api.moveBlockToOtherTrack(dragBlockId, targetTrack.staticExportItem["id"], newBlockOrder)
elif ccBlock: #TODO: Also different CC in the same GuiTrack
api.moveCCBlockToOtherTrack(dragBlockId, targetTrack.parentDataTrackId, newBlockOrder)
elif event.button() == 1: #a positional mouse left click in a note-track
track = self.trackAt(event.scenePos())
if track and not track is self.conductor:
modifiers = QtWidgets.QApplication.keyboardModifiers()
trackId = track.staticExportItem["id"]
if modifiers == QtCore.Qt.ShiftModifier:
api.selectToTickindex(trackId, event.scenePos().x() * constantsAndConfigs.ticksToPixelRatio)
else:
api.toTickindex(trackId, event.scenePos().x() * constantsAndConfigs.ticksToPixelRatio)
super().mouseReleaseEvent(event)

15083
qtgui/resources.py

File diff suppressed because it is too large

498
qtgui/scorescene.py

@ -0,0 +1,498 @@
#! /usr/bin/env python3
# -*- coding: utf-8 -*-
"""
Copyright 2019, Nils Hilbricht, Germany ( https://www.hilbricht.net )
This file is part of the Laborejo Software Suite ( https://www.laborejo.org ),
more specifically its template base application.
Laborejo2 is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
"""
import logging; logging.info("import {}".format(__file__))
#Standard Library
#Third party
from PyQt5 import QtCore, QtGui, QtWidgets
#Template
#Our own files
import engine.api as api
from .constantsAndConfigs import constantsAndConfigs
from .grid import GuiGrid
from .conductor import Conductor, ConductorTransparentBlock
from .musicstructures import GuiBlockHandle, GuiTrack
from .graphs import CCGraphTransparentBlock
from .cursor import Cursor, Playhead, Selection
class GuiScore(QtWidgets.QGraphicsScene):
def __init__(self, parentView):
super().__init__()
self.parentView = parentView
self.setItemIndexMethod(QtWidgets.QGraphicsScene.NoIndex)
self.tracks = {} #trackId:guiTrack, #if we don't save the instances here in Python space Qt will loose them and they will not be displayed without any error message.
self.deleteOnIdleStack = [] # a stack that holds hidden items that need to be deleted. Hiding is much faster than deleting so we use that for the blocking function. Since we always recreate items and never re-use this is ok as a list. no need for a set.
self._deleteOnIdleLoop = QtCore.QTimer()
self._deleteOnIdleLoop.start(0) #0 means "if there is time"
self._deleteOnIdleLoop.timeout.connect(self._deleteOnIdle) #processes deleteOnIdleStack
self.duringTrackDragAndDrop = None #switched to a QGraphicsItem (e.g. GuiTrack) while a track is moved around by the mouse
self.duringBlockDragAndDrop = None #switched to a QGraphicsItem (e.g. GuiTrack) while a block is moved around by the mouse
self.conductor = Conductor(parentView = self.parentView)
self.addItem(self.conductor)
self.conductor.setPos(0, -1 * self.conductor.totalHeight)
self.yStart = self.conductor.y() - self.conductor.totalHeight/2
self.hiddenTrackCounter = QtWidgets.QGraphicsSimpleTextItem("") #filled in by self.redraw on callback tracksChanged (loading or toggling visibility of backend tracks)
self.addItem(self.hiddenTrackCounter)
self.backColor = QtGui.QColor()
self.backColor.setNamedColor("#fdfdff")
self.setBackgroundBrush(self.backColor)
self.grid = GuiGrid(parent=self)
self.addItem(self.grid)
self.grid.setPos(0, -20 * constantsAndConfigs.stafflineGap) #this is more calculation than simply using self.yStart, and might require manual adjustment in the future, but at least it guarantees the grid matches the staffline positions
self.grid.setZValue(-50)
self.cachedSceneHeight = 0 #set in self.redraw. Used by updateTrack to set the sceneRect
#All Cursors
self.cursor = Cursor()
self.addItem(self.cursor)
self.selection = Selection()
self.addItem(self.selection)
self.playhead = Playhead(self)
self.addItem(self.playhead)
self.playhead.setY(self.yStart)
#Callbacks
api.callbacks.tracksChanged.append(self.redraw)
api.callbacks.updateTrack.append(self.updateTrack)
api.callbacks.updateBlockTrack.append(self.trackPaintBlockBackgroundColors)
api.callbacks.updateGraphTrackCC.append(self.updateGraphTrackCC)
api.callbacks.updateGraphBlockTrack.append(self.updateGraphBlockTrack)
api.callbacks.graphCCTracksChanged.append(self.syncCCsToBackend)
def updateMode(self, nameAsString):
assert nameAsString in constantsAndConfigs.availableEditModes
for track in self.tracks.values():
track.updateMode(nameAsString)
self.grid.updateMode(nameAsString)
def maxTrackLength(self):
if self.tracks:
return max(tr.lengthInPixel for tr in self.tracks.values())
#return max(max(tr.lengthInPixel for tr in self.tracks.values()), self.parentView.geometry().width())
#return max(max(tr.lengthInPixel for tr in self.scene().tracks.values()), self.scene().parentView.geometry().width()) + self.scene().parentView.geometry().width()
else:
return 0 #self.parentView.geometry().width()
def updateSceneRect(self):
self.parentView.setSceneRect(QtCore.QRectF(-5, self.yStart, self.maxTrackLength() + 300, self.cachedSceneHeight)) #x,y,w,h
def updateTrack(self, trackId, staticRepresentationList):
"""for callbacks"""
if trackId in self.tracks:
self.tracks[trackId].redraw(staticRepresentationList)
#else:
#hidden track. But this can still happen through the data editor
self.parentView.updateMode()
self.updateSceneRect()
def trackPaintBlockBackgroundColors(self, trackId, staticBlocksRepresentation):
if trackId in self.tracks:
self.tracks[trackId].paintBlockBackgroundColors(staticBlocksRepresentation)
#else:
#hidden track.
def updateGraphTrackCC(self, trackId, ccNumber, staticRepresentationList):
"""TrackId is a real notation track which has a dict of CCs"""
if trackId in self.tracks:
track = self.tracks[trackId]
ccPath = track.ccPaths[ccNumber]
ccPath.createGraphicItemsFromData(staticRepresentationList)
def updateGraphBlockTrack(self, trackId, ccNumber, staticRepresentationList):
"""TrackId is a real notation track which has a dict of CCs"""
if trackId in self.tracks:
self.tracks[trackId].ccPaths[ccNumber].updateGraphBlockTrack(staticRepresentationList)