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.

243 lines
12 KiB

4 years ago
#! /usr/bin/env python3
# -*- coding: utf-8 -*-
"""
Copyright 2017, Nils Hilbricht, Germany ( https://www.hilbricht.net )
This file is part of Laborejo ( https://www.laborejo.org )
Laborejo 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/>.
"""
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)
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.setAcceptedMouseButtons(QtCore.Qt.LeftButton)
self.setZValue(90)
#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()
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.setPen(pen)
self.setOpacity(0.2)
self.setEnabled(False)
api.callbacks.setSelection.append(self.setSelection)
def setSelection(self, 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)