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.
258 lines
13 KiB
258 lines
13 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")
|
|
|
|
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(int(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._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)
|
|
|