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.
 
 
 
 
 
 

193 lines
8.0 KiB

#! /usr/bin/env python3
# -*- coding: utf-8 -*-
"""
Copyright 2021, Nils Hilbricht, Germany ( https://www.hilbricht.net )
This file is part of the Laborejo Software Suite ( https://www.laborejo.org ),
This 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
from math import isclose
#Third Party
from PyQt5 import QtCore, QtGui, QtWidgets
#Template Modules
#Our Modules
from .score import Score
import engine.api as api
from .constantsAndConfigs import constantsAndConfigs
class ScoreView(QtWidgets.QGraphicsView):
def __init__(self, mainWindow):
super().__init__()
self.mainWindow = mainWindow
viewport = QtWidgets.QOpenGLWidget()
viewportFormat = QtGui.QSurfaceFormat()
viewportFormat.setSwapInterval(0) #disable VSync
#viewportFormat.setSamples(2**8) #By default, the highest number of samples available is used.
viewportFormat.setDefaultFormat(viewportFormat)
viewport.setFormat(viewportFormat)
self.setViewport(viewport)
self.setAlignment(QtCore.Qt.AlignLeft|QtCore.Qt.AlignBottom)
style = """
QScrollBar:horizontal {
border: 1px solid black;
}
QScrollBar::handle:horizontal {
background: #00b2b2;
}
QScrollBar:vertical {
border: 1px solid black;
}
QScrollBar::handle:vertical {
background: #00b2b2;
}
"""
self.setStyleSheet(style)
self.setLineWidth(0)
self.scoreScene = Score(self) #Must use the variable scoreScene and after super init
self.setScene(self.scoreScene)
self.setDragMode(QtWidgets.QGraphicsView.NoDrag) #Rubber band is dynamically switched on by holding shift
self.rubberBandChanged.connect(self.scoreScene.react_RubberBandChanged)
self.centerOn(self.scoreScene.playhead) #A good starting point to look at.
self._oldWidth = 0
self._cachedSongDurationInPixel = 0
api.callbacks.setPlaybackTicks.append(self._decideIfToGrowSceneRect) #Is the cursor outside the song?
api.callbacks.songDurationChanged.append(self._songDurationChanged)
self.setHorizontalScrollBarPolicy(QtCore.Qt.ScrollBarAlwaysOff) #Because we hook into the velocity views srollbar
#ScreenRect is set dynamically in self._decideIfToGrowSceneRect
#self.setFixedHeight(constantsAndConfigs.scoreHeight)
def wheelEvent(self, event):
if QtWidgets.QApplication.keyboardModifiers() in (QtCore.Qt.ControlModifier, QtCore.Qt.ControlModifier|QtCore.Qt.ShiftModifier): #a workaround for a qt bug. see score.wheelEvent docstring.
event.ignore() #do not send to scene, but tell the mainWindow to use it.
else:
super().wheelEvent(event) #send to scene
def toggleFollowPlayhead(self):
constantsAndConfigs.followPlayhead = not constantsAndConfigs.followPlayhead
self.mainWindow.ui.actionFollow_Playhead.setChecked(constantsAndConfigs.followPlayhead)
#we register a callback in self init that checks constantsAndConfigs.followPlayhead
def enterEvent(self, event):
self.scoreScene.inputCursor.show()
super().enterEvent(event)
def leaveEvent(self, event):
"""Hide the note stamp . otherwise it looks strange if you move over the piano and it stays
on the last position"""
self.scoreScene.inputCursor.hide()
super().leaveEvent(event)
def centerOnCursor(self, cursorExportObject):
"""Needs to be reimplemented from template. Or ignored."""
pass
def centerOnPlayhead(self):
x = api.getPlaybackTicks() / constantsAndConfigs.ticksToPixelRatio
self.centerOn(x, self.scoreScene.sceneRect().center().y() )
def mouseMoveEvent(self, event):
super().mouseMoveEvent(event)
selPos = self.scoreScene.lastEnd
if selPos: #during selection make sure the selection scrolls the ScrollArea.
viewPos = self.mapFromScene(selPos)
#self.centerOn(selPos.x(), self.sceneRect().center().y()) #Way too fast!
#Scrollspeed depends on the zoomFactor
if viewPos.x() < 5:
self.horizontalScrollBar().setValue(self.horizontalScrollBar().value() - int(50 /constantsAndConfigs.zoomFactor))
elif viewPos.x() > self.geometry().width()-15:
self.horizontalScrollBar().setValue(self.horizontalScrollBar().value() + int(50 /constantsAndConfigs.zoomFactor))
#Y zooming is slower in general
if viewPos.y() < 5:
self.verticalScrollBar().setValue(self.verticalScrollBar().value() - int(25 /constantsAndConfigs.zoomFactor))
elif viewPos.y() > self.geometry().height()-15:
self.verticalScrollBar().setValue(self.verticalScrollBar().value() + int(25 /constantsAndConfigs.zoomFactor))
def mousePosAsTickposition(self)->int:
return self.scoreScene.lastMouseScenePos.x() * constantsAndConfigs.ticksToPixelRatio
def _mousePressEvent(self, event):
"""Event order for GraphicsView and GraphicsScene is:
view catches before scene, cannot be prevented
scene catches before its items.
the call to super propagates down: Calling super in the view triggers the scene.
Then control returns to this function.
However, sending the event to the scene means we loose the event and cannot communicate with
the scene per event.accept() because the scene has QGraphicsMouseEvent and we do not.
This means we need handle selection destroying in the scene.
"""
#IN CASE YOU MISSED IT: THIS FUNCTION IS NOT IN USE. KEEP FOR DOCSTRING
super().mousePressEvent(event) #send to scene.
def stretchXCoordinates(self, factor:float):
self._cachedSongDurationInPixel *= factor
self._decideIfToGrowSceneRect(False, False)
self.scoreScene.stretchXCoordinates(factor)
x = self.scoreScene.lastMouseScenePos.x()
self.centerOn(x, self.scoreScene.sceneRect().center().y() )
def zoom(self, factor):
"""Factor is absolute. We reset before setting the new scale"""
assert factor == constantsAndConfigs.zoomFactor
self.resetTransform()
self.scale(factor, factor)
x = self.scoreScene.lastMouseScenePos.x()
self.centerOn(x, self.scoreScene.sceneRect().center().y())
def _decideIfToGrowSceneRect(self, tickindex:int, playbackStatus:bool):
"""The scene itself is extremely long.
The grids spans to the maximum theoretical song length.
This function only shows the part with actual events or the playhead.
Whatever is further to the right"""
width = max(self._cachedSongDurationInPixel, self.scoreScene.playhead.pos().x(), self.geometry().width())
update = not isclose(self._oldWidth, width)
if update:
self._oldWidth = width
r = self.sceneRect()
r.setWidth(width+300) #previously we had widht*1.5 here to keep the playback cursor in the middle of the screen. #however, this does widen exponentially *1.5 which is bad for performance
#and also the screen extends needlesly to the right
self.setSceneRect(r)
self.scoreScene.grid.adjustRhythmLines()
def _songDurationChanged(self, firstEvent:int, lastEvent:int):
self._cachedSongDurationInPixel = lastEvent / constantsAndConfigs.ticksToPixelRatio