#! /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 . """ 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)