You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
292 lines
12 KiB
292 lines
12 KiB
#! /usr/bin/env python3
|
|
# -*- coding: utf-8 -*-
|
|
"""
|
|
Copyright 2022, Nils Hilbricht, Germany ( https://www.hilbricht.net )
|
|
|
|
This file is part of the Laborejo Software Suite ( https://www.laborejo.org ),
|
|
|
|
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; logger = logging.getLogger(__name__); logger.info("import")
|
|
|
|
|
|
|
|
#Standard Library
|
|
|
|
#Third Party
|
|
from PyQt5 import QtCore, QtGui, QtWidgets
|
|
|
|
#Our Template Modules
|
|
from template.qtgui.helper import stretchLine
|
|
|
|
#Client Modules
|
|
from .constantsAndConfigs import constantsAndConfigs
|
|
import engine.api as api
|
|
|
|
|
|
gridPen = QtGui.QPen(QtCore.Qt.DotLine)
|
|
gridPen.setCosmetic(True)
|
|
|
|
masterLine = QtCore.QLineF(0, 0, 0, 128*constantsAndConfigs.stafflineGap) # (x1, y1, x2, y2)
|
|
class RhythmLine(QtWidgets.QGraphicsLineItem):
|
|
def __init__(self, parentGrid):
|
|
super().__init__(masterLine)
|
|
self.setEnabled(False)
|
|
self.setParentItem(parentGrid)
|
|
self.setAcceptedMouseButtons(QtCore.Qt.NoButton) #we still need this otherwise no rubberband.
|
|
|
|
class ActualGrid(QtWidgets.QGraphicsItem):
|
|
|
|
def boundingRect(self):
|
|
return self.childrenBoundingRect()
|
|
|
|
class GuiGrid(QtWidgets.QGraphicsItem):
|
|
"""Third version of the grid.
|
|
Version 1: QGraphicsItemGroup with horizontal and vertical lines. Slow, disturbing to look at
|
|
even when the horizontal lines where nice ledger lines.
|
|
Drawing all lines for the whole score.
|
|
|
|
Version 2: QGraphicsItemGroup only vertical lines.
|
|
Only a few lines were created and were moved on scrolling.
|
|
QGraphicsItemGroupp.addToGroup and the constant updating was still too much.
|
|
|
|
Version 3: QGraphicsItem. Only vertical. Draw full scene. No scrolling trickery.
|
|
As simple as possible. Leave as much of the work to Qt.
|
|
We have a subitem that is actually the grid, so we can show/hide/delete that at once.
|
|
|
|
"""
|
|
|
|
def __init__(self, parentScene):
|
|
super(GuiGrid, self).__init__()
|
|
self.parent = parentScene #QGraphicsScene
|
|
self.setFlag(QtWidgets.QGraphicsItem.ItemHasNoContents, True) #only child items. Without this we get notImplementedError: QGraphicsItem.paint() is abstract and must be overridden
|
|
self.setAcceptedMouseButtons(QtCore.Qt.NoButton)
|
|
self.setEnabled(False)
|
|
self.setOpacity(constantsAndConfigs.gridOpacity)
|
|
self.actualGridItem = None
|
|
self.nrOfVerticalLines = 0 #remember for optimisations
|
|
self._rememberSceneHeight = 0
|
|
|
|
|
|
def reactOnScoreChange(self):
|
|
"""Entry point for callbacks and parent functions"""
|
|
self.redrawTickGrid()
|
|
|
|
def boundingRect(self):
|
|
return self.childrenBoundingRect()
|
|
|
|
def updateMode(self, nameAsString):
|
|
assert nameAsString in constantsAndConfigs.availableEditModes
|
|
if nameAsString == "block":
|
|
self.hide()
|
|
else:
|
|
self.show()
|
|
|
|
def redrawTickGrid(self):
|
|
"""A complete redraw when triggered."""
|
|
|
|
gapVertical = constantsAndConfigs.gridRhythm / constantsAndConfigs.ticksToPixelRatio #this is necessary every time after stretching (ticksToPixelRatio) and after changing the gridRhythm
|
|
width = self.parent.maxTrackLength()
|
|
height = self.parent.cachedSceneHeight
|
|
|
|
lines = int(width / gapVertical) + 8 # + extra ones for a good feeling
|
|
|
|
if lines == self.nrOfVerticalLines and self._rememberSceneHeight == height:
|
|
return
|
|
else: #change
|
|
self.nrOfVerticalLines = lines
|
|
self._rememberSceneHeight = height
|
|
|
|
try:
|
|
self.parent.removeWhenIdle(self.actualGridItem) #remove all at once
|
|
except:
|
|
pass #will fail the first time on start
|
|
|
|
self.actualGridItem = ActualGrid(self)
|
|
self.actualGridItem.setFlag(QtWidgets.QGraphicsItem.ItemHasNoContents, True) #only child items. Without this we get notImplementedError: QGraphicsItem.paint() is abstract and must be overridden
|
|
self.actualGridItem.setParentItem(self)
|
|
self.actualGridItem.setPos(0,0)
|
|
|
|
for i in range(lines):
|
|
l = QtWidgets.QGraphicsLineItem(0,0,0, height, self.actualGridItem) #x1, y1, x2, y2, parent
|
|
l.setPos(i * gapVertical, 0)
|
|
|
|
def reactToresizeEventOrZoom(self):
|
|
"""Called by the Views resizeEvent.
|
|
When the views geometry changes or zooms"""
|
|
pass
|
|
|
|
def stretchXCoordinates(self, factor):
|
|
"""Reposition the items on the X axis.
|
|
Call goes through all parents/children, starting from ScoreView._stretchXCoordinates.
|
|
Docstring there."""
|
|
gapVertical = constantsAndConfigs.gridRhythm / constantsAndConfigs.ticksToPixelRatio #this is necessary every time after stretching (ticksToPixelRatio) and after changing the gridRhythm
|
|
|
|
for vline in self.actualGridItem.childItems(): #rhythm- and barlines
|
|
vline.setX(vline.x() * factor)
|
|
|
|
|
|
class OldGuiGrid(QtWidgets.QGraphicsItemGroup):
|
|
#Don't use that. QGraphicsItemGroup is very slow.
|
|
"""The grid consists of vertical lines.
|
|
lines help to estimate
|
|
the rhythm or tick positions e.g. when trying to find the right place for a tempo change or CC.
|
|
|
|
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, parentScene):
|
|
super(GuiGrid, self).__init__()
|
|
self.parent = parentScene #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.width = None #recalculated in reactToresizeEventOrZoom
|
|
self.height = None #recalculated in reactToresizeEventOrZoom
|
|
self.reactToresizeEventOrZoom()
|
|
|
|
self.linesVertical = [] #later the grid lines will be stored here
|
|
|
|
self.verticalScrollbarWaitForGapJump = 0
|
|
self.oldVerticalValue = 0
|
|
|
|
self.parent.parentView.verticalScrollBar().valueChanged.connect(self.reactToVerticalScroll)
|
|
|
|
self.setOpacity(constantsAndConfigs.gridOpacity)
|
|
|
|
gridPen = QtGui.QPen(QtCore.Qt.DotLine)
|
|
gridPen.setCosmetic(True)
|
|
|
|
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 lines to a new position,
|
|
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.gapVertical: #collect scrollpixels until we scrolled more than one gap
|
|
gapMultiplier, rest = divmod(self.verticalScrollbarWaitForGapJump, self.gapVertical) #really fast scrolling can jump more than one gap
|
|
self.verticalScrollbarWaitForGapJump = rest #keep the rest for the next scroll
|
|
assert abs(self.verticalScrollbarWaitForGapJump) < self.gapVertical
|
|
|
|
|
|
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.height() #Use the scene, not the view. The view (in the past) was very wrong.
|
|
scoreViewWidgetWidth = self.parent.width()
|
|
|
|
if (not self.height) or scoreViewWidgetHeight > self.height:
|
|
self.height = scoreViewWidgetHeight
|
|
if (not self.width) or scoreViewWidgetWidth > self.width:
|
|
self.width = scoreViewWidgetWidth
|
|
|
|
if self.initialGridExists:
|
|
assert self.linesVertical, self.linesVertical
|
|
assert self.parent.parentView.isVisible(), self.parent.parentView.isVisible()
|
|
self._fillVerticalLines()
|
|
|
|
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 highest
|
|
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 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 necessary every time after stretching (ticksToPixelRatio) and after changing the gridRhythm
|
|
|
|
for vline in self.linesVertical: #rhythm- and barlines
|
|
vline.setX(vline.x() * factor)
|
|
|
|
self._fillVerticalLines()
|
|
|
|
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, 0, 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 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 user changes the tick rhythm manually.
|
|
"""
|
|
|
|
if self.initialGridExists:
|
|
#it is possible that the grid did not change. proceed nevertheless
|
|
for l in self.linesVertical:
|
|
self.parent.removeWhenIdle(l)
|
|
|
|
|
|
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.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.initialGridExists = True
|
|
|