Nils
5 years ago
20 changed files with 6793 additions and 9737 deletions
@ -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 |
|||
|
@ -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 |
File diff suppressed because it is too large
@ -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) |
|||
|
|||