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.

260 lines
13 KiB

4 years ago
#! /usr/bin/env python3
# -*- coding: utf-8 -*-
"""
11 months ago
Copyright 2022, Nils Hilbricht, Germany ( https://www.hilbricht.net )
4 years ago
4 years ago
This file is part of the Laborejo Software Suite ( https://www.laborejo.org ),
4 years ago
4 years ago
Laborejo2 is free software: you can redistribute it and/or modify
4 years ago
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")
4 years ago
4 years ago
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)
4 years ago
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
4 years ago
#Cursor.stafflineEffect = QtWidgets.QGraphicsDropShadowEffect()
#Cursor.stafflineEffect.setColor(QtGui.QColor("black"))
#Cursor.stafflineEffect.setOffset(0,0)
#Cursor.stafflineEffect.setBlurRadius(5)
4 years ago
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)
4 years ago
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)
4 years ago
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)
4 years ago
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)
4 years ago
self.setPen(p)
#self.setAcceptHoverEvents(True)
4 years ago
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)
4 years ago
self.setAcceptedMouseButtons(QtCore.Qt.LeftButton)
self.setZValue(90) #This is relative to the parent, which is the scene.
4 years ago
#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()
offset = api.D1 / constantsAndConfigs.ticksToPixelRatio
if cursorViewPosX <= 0 or cursorViewPosX >= width-offset: #"pageflip" with offset to show the last measure (or so) twice
self.parentScoreScene.parentView.horizontalScrollBar().setValue(int(x * constantsAndConfigs.zoomFactor))
4 years ago
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)
4 years ago
#def hoverEnterEvent(self, event):
# self.setCursor(QtCore.Qt.SizeHorCursor)
# self.update() #the default implementation calls this. event.accept/ignore has no effect.
4 years ago
#def hoverLeaveEvent(self, event):
# self.unsetCursor()
# self.update() #the default implementation calls this. event.accept/ignore has no effect.
4 years ago
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
4 years ago
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.
4 years ago
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)
4 years ago
def setSelection(self, tupleOfCursorExportObjects):
self._tupleOfCursorExportObjects = tupleOfCursorExportObjects
4 years ago
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)