#! /usr/bin/env python3
# -*- coding: utf-8 -*-
"""
Copyright 2020 , 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 " )
from PyQt5 import QtCore , QtGui , QtWidgets , QtWidgets
from . constantsAndConfigs import constantsAndConfigs
import engine . api as api
pen = QtGui . QPen ( )
pen . setCapStyle ( QtCore . Qt . RoundCap )
pen . setJoinStyle ( QtCore . Qt . RoundJoin )
pen . setWidth ( 2 )
#pen.setColor(QtGui.QColor("red"))
class PitchCursor ( QtWidgets . QGraphicsRectItem ) :
def __init__ ( self ) :
""" Does not need the actual dotOnLine.
this is done by the parent cursor .
This is just a fancy rect . """
#super().__init__(constantsAndConfigs.magicPixel, 2, constantsAndConfigs.magicPixel*3, constantsAndConfigs.stafflineGap/3)
super ( ) . __init__ ( 0 , 1 , constantsAndConfigs . magicPixel * 3 , 5 )
self . setBrush ( QtGui . QColor ( " cyan " ) )
self . setPen ( pen )
self . setEnabled ( False )
class PositionCursor ( QtWidgets . QGraphicsRectItem ) :
def __init__ ( self ) :
""" Does not need the actual position.
this is done by the parent cursor
This is just a fancy rect . """
super ( ) . __init__ ( - 1.5 * constantsAndConfigs . magicPixel , - 3.5 * constantsAndConfigs . stafflineGap , constantsAndConfigs . magicPixel * 1.5 , 7 * constantsAndConfigs . stafflineGap )
self . setBrush ( QtGui . QColor ( " cyan " ) )
self . setPen ( pen )
self . setScale ( 0.8 )
self . setEnabled ( False )
class Cursor ( QtWidgets . QGraphicsItemGroup ) :
""" A cursor that shows the vertical
as well as the horizontal position
"""
def __init__ ( self ) :
super ( ) . __init__ ( )
self . cursorExportObject = None #the last cached cursor object aka the current cursor position
self . pitch = PitchCursor ( )
self . addToGroup ( self . pitch )
self . position = PositionCursor ( )
self . addToGroup ( self . position )
api . callbacks . setCursor . append ( self . setCursor )
self . setEnabled ( False )
def clearItemHighlight ( self ) :
""" Gets called before a track changes. Most of the time when a new item is inserted/deleted.
This means the gui track will be recreated and a current highlight on an item might get
deleted while still on the item . This results in a qt crash
"""
Cursor . hightlightEffect = QtWidgets . QGraphicsColorizeEffect ( ) #default strength of the effect is 1.0
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.QGraphicsDropShadowEffect()
#Cursor.stafflineEffect.setColor(QtGui.QColor("black"))
#Cursor.stafflineEffect.setOffset(0,0)
#Cursor.stafflineEffect.setBlurRadius(5)
def setCursor ( self , cursorExportObject ) :
self . cursorExportObject = cursorExportObject
self . scene ( ) . parentView . setFocus ( )
self . pitch . setY ( constantsAndConfigs . stafflineGap * cursorExportObject [ " dotOnLine " ] / 2 - 3 ) #the same as a notehead
x = cursorExportObject [ " tickindex " ] / constantsAndConfigs . ticksToPixelRatio
#Finally shift the Position to the correct track. Trackindex from 0, naturally.
#self.setPos(x, constantsAndConfigs.timeLineOffsetNoteEditor + cursorExportObject["trackIndex"] * constantsAndConfigs.trackHeight)
currentGuiTrack = self . scene ( ) . tracks [ cursorExportObject [ " trackId " ] ]
self . setPos ( x , currentGuiTrack . y ( ) )
try :
guiItemAtCursor = currentGuiTrack . itemById ( cursorExportObject [ " itemId " ] )
if not guiItemAtCursor . staticItem [ " completeDuration " ] > 0 :
#guiItemAtCursor has no duration. is type guiItemAtCursor.staticItem["type"]
Cursor . hightlightEffect . setEnabled ( True )
guiItemAtCursor . setGraphicsEffect ( Cursor . hightlightEffect )
else :
Cursor . hightlightEffect . setEnabled ( False )
except StopIteration :
Cursor . hightlightEffect . setEnabled ( False ) #happens only on startup or appending (which is the same)
#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)
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)
class Playhead ( QtWidgets . QGraphicsLineItem ) :
def __init__ ( self , parentScoreScene ) :
super ( ) . __init__ ( 0 , 0 , 0 , 0 ) # (x1, y1, x2, y2)
self . parentScoreScene = parentScoreScene
p = QtGui . QPen ( )
p . setColor ( QtGui . QColor ( " red " ) )
p . setCosmetic ( True )
p . setWidth ( 3 )
self . setPen ( p )
#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 ) #This is relative to the parent, which is the scene.
#self.parentScoreScene.parentView.verticalScrollBar().valueChanged.connect(self.setLineToWindowHeigth)
#self.hide()
#self.maxHeight = QtWidgets.QDesktopWidget().geometry().height() #we really hope the screen resolution does not change during the session.
def setCursorPosition ( self , tickindex : int , playbackStatus : bool ) :
""" Set the playhead to the right position, but keep the viewport stable.
Shift the entire " page " if the cursor becomes invisible because its steps outside the viewport """
self . setLineToWindowHeigth ( )
x = tickindex / constantsAndConfigs . ticksToPixelRatio
self . setX ( x )
if constantsAndConfigs . followPlayhead and playbackStatus :
scenePos = self . parentScoreScene . parentView . mapFromScene ( self . pos ( ) )
cursorViewPosX = scenePos . x ( ) #the cursor position in View coordinates
width = self . parentScoreScene . parentView . geometry ( ) . width ( )
if cursorViewPosX < = 0 or cursorViewPosX > = width : #"pageflip"
self . parentScoreScene . parentView . horizontalScrollBar ( ) . setValue ( x * constantsAndConfigs . zoomFactor )
def _Deprecated__ScrollingVariant___setCursorPosition ( self , tickindex ) :
""" the tickindex to pixel index on the x axis is a fixed 1:n
relation . What you see is where you are . No jumps , the playhead
stays on course . """
self . setLineToWindowHeigth ( )
x = tickindex / constantsAndConfigs . ticksToPixelRatio
self . setX ( x )
if constantsAndConfigs . followPlayhead and api . playbackStatus ( ) :
#self.parentScoreScene.parentView.centerOn(self) Do not use center on. It centers for Y as well which creates a recursion and the score gets taller and taller.
#self.scene().parentView.horizontalScrollBar().setValue(x - 150)
#xV = self.parentScoreScene.parentView.mapFromScene(x,0).x()
self . parentScoreScene . parentView . horizontalScrollBar ( ) . setValue ( x - 150 ) #x does not with zoom levels. the bar drifts away.
def setLineToWindowHeigth ( self , * args ) :
h = self . parentScoreScene . cachedSceneHeight
self . setLine ( 0 , 0 , 0 , h ) #(x1, y1, x2, y2)
def mouseMoveEvent ( self , event ) :
""" Only allow movement in Y direction.
Only triggered when dragging . """
#super().mouseMoveEvent(event) allows free movement through Qt. Don't call that.
p = event . scenePos ( ) . x ( )
if p < 0 :
p = 0
#self.setPos(p, self.scene().parentView.mapToScene(0, 0).y())
self . setX ( p )
api . seek ( p * constantsAndConfigs . ticksToPixelRatio )
event . accept ( )
#TODO: grid does not exist anymore
#def mouseReleaseEvent(self, event):
# if constantsAndConfigs.snapToGrid:
# x = event.scenePos().x() * constantsAndConfigs.ticksToPixelRatio
# p = round(x / constantsAndConfigs.gridRhythm) * constantsAndConfigs.gridRhythm
# if p < 0:
# 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 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 """
def __init__ ( self ) :
super ( ) . __init__ ( 0 , 0 , 0 , 0 )
self . validBrush = QtGui . QBrush ( QtGui . QColor ( " grey " ) )
self . invalidBrush = QtGui . QBrush ( QtGui . QColor ( " grey " ) )
self . invalidBrush . setStyle ( QtCore . Qt . DiagCrossPattern )
self . _tupleOfCursorExportObjects = None #cache for stretchX
self . setPen ( pen )
self . setOpacity ( 0.2 )
self . setZValue ( 95 ) #Below playback cursor, but pretty high. This is relative to the parent, which is the scene.
self . setEnabled ( False )
api . callbacks . setSelection . append ( self . setSelection )
def stretchXCoordinates ( self , factor ) :
""" Reposition the items on the X axis.
Call goes through all parents / children , starting from ScoreView . _stretchXCoordinates .
Docstring there . """
self . setSelection ( self . _tupleOfCursorExportObjects )
def setSelection ( self , tupleOfCursorExportObjects ) :
self . _tupleOfCursorExportObjects = tupleOfCursorExportObjects
if tupleOfCursorExportObjects :
validSelection , topleftCursorObject , bottomRightCursorObject = tupleOfCursorExportObjects
if validSelection :
self . setBrush ( self . validBrush )
else :
self . setBrush ( self . invalidBrush )
topGuiTrack = self . scene ( ) . tracks [ topleftCursorObject [ " trackId " ] ]
bottomGuiTrack = self . scene ( ) . tracks [ bottomRightCursorObject [ " trackId " ] ]
#y = constantsAndConfigs.timeLineOffsetNoteEditor + topleftCursorObject["trackIndex"] * constantsAndConfigs.trackHeight - constantsAndConfigs.trackHeight/2
#h = (bottomRightCursorObject["trackIndex"] - topleftCursorObject["trackIndex"]) * constantsAndConfigs.trackHeight + constantsAndConfigs.trackHeight
x = topleftCursorObject [ " tickindex " ] / constantsAndConfigs . ticksToPixelRatio
w = ( bottomRightCursorObject [ " tickindex " ] - topleftCursorObject [ " tickindex " ] ) / constantsAndConfigs . ticksToPixelRatio
y = topGuiTrack . y ( ) - constantsAndConfigs . trackHeight / 2
if bottomGuiTrack . staticExportItem [ " double " ] :
h = bottomGuiTrack . y ( ) - y + constantsAndConfigs . trackHeight
else :
h = bottomGuiTrack . y ( ) - y + constantsAndConfigs . trackHeight / 2
self . setRect ( 0 , 0 , w - 3 , h ) #substract a few pixels to make it look less ambigious if the last item on the right edge is included or not (it is not)
self . setPos ( x , y )
self . setVisible ( True )
else : #no selection
self . setVisible ( False )