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.
599 lines
30 KiB
599 lines
30 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")
|
|
|
|
#Standard Library
|
|
|
|
#Third party
|
|
from PyQt5 import QtCore, QtGui, QtWidgets
|
|
|
|
#Template
|
|
|
|
#Our own files
|
|
import engine.api as api
|
|
|
|
from .constantsAndConfigs import constantsAndConfigs
|
|
from .grid import GuiGrid
|
|
from .conductor import Conductor, ConductorTransparentBlock
|
|
from .musicstructures import GuiBlockHandle, GuiTrack
|
|
from .graphs import CCGraphTransparentBlock, CCPath
|
|
from .cursor import Cursor, Playhead, Selection
|
|
|
|
|
|
class GuiScore(QtWidgets.QGraphicsScene):
|
|
def __init__(self, parentView):
|
|
super().__init__()
|
|
self.parentView = parentView
|
|
self.setItemIndexMethod(QtWidgets.QGraphicsScene.NoIndex)
|
|
|
|
|
|
self.tracks = {} #trackId:guiTrack, #if we don't save the instances here in Python space Qt will loose them and they will not be displayed without any error message.
|
|
|
|
self.deleteOnIdleStack = set() # a stack that holds hidden items that need to be deleted. Hiding is much faster than deleting so we use that for the blocking function.
|
|
self._deleteOnIdleLoop = QtCore.QTimer()
|
|
self._deleteOnIdleLoop.start(100) #0 means "if there is time"
|
|
self._deleteOnIdleLoop.timeout.connect(self._deleteOnIdle) #processes deleteOnIdleStack
|
|
|
|
self.duringTrackDragAndDrop = None #switched to a QGraphicsItem (e.g. GuiTrack) while a track is moved around by the mouse
|
|
self.duringBlockDragAndDrop = None #switched to a QGraphicsItem (e.g. GuiTrack) while a block is moved around by the mouse
|
|
self.mouseMoveEventBlocked = False #mouseMoveEvent and helper functions
|
|
|
|
self.conductor = Conductor(parentView = self.parentView)
|
|
self.addItem(self.conductor)
|
|
self.conductor.setPos(0, -1 * self.conductor.totalHeight)
|
|
|
|
self.yStart = self.conductor.y() - self.conductor.totalHeight/2
|
|
|
|
self.hiddenTrackCounter = QtWidgets.QGraphicsSimpleTextItem("") #filled in by self.redraw on callback tracksChanged (loading or toggling visibility of backend tracks)
|
|
self.addItem(self.hiddenTrackCounter)
|
|
|
|
self.backColor = QtGui.QColor()
|
|
self.backColor.setNamedColor("#fdfdff")
|
|
self.setBackgroundBrush(self.backColor)
|
|
|
|
self.cachedSceneHeight = 0 #set in self.redraw. Used by updateTrack to set the sceneRect
|
|
|
|
self.grid = GuiGrid(parentScene=self)
|
|
self.addItem(self.grid)
|
|
self.grid.setPos(0, -20 * constantsAndConfigs.stafflineGap) #this is more calculation than simply using self.yStart, and might require manual adjustment in the future, but at least it guarantees the grid matches the staffline positions
|
|
self.grid.setZValue(-50)
|
|
|
|
|
|
|
|
#All Cursors
|
|
self.cursor = Cursor()
|
|
self.addItem(self.cursor)
|
|
self.cursor.setZValue(90)
|
|
self.selection = Selection()
|
|
self.selection.setZValue(80)
|
|
self.addItem(self.selection)
|
|
self.playhead = Playhead(self)
|
|
self.addItem(self.playhead)
|
|
self.playhead.setY(self.yStart)
|
|
self.playhead.setZValue(100)
|
|
|
|
#Callbacks
|
|
api.callbacks.tracksChanged.append(self.redraw)
|
|
api.callbacks.updateTrack.append(self.updateTrack)
|
|
api.callbacks.updateBlockTrack.append(self.trackPaintBlockBackgroundColors)
|
|
|
|
api.callbacks.updateGraphTrackCC.append(self.updateGraphTrackCC)
|
|
api.callbacks.updateGraphBlockTrack.append(self.updateGraphBlockTrack)
|
|
api.callbacks.graphCCTracksChanged.append(self.syncCCsToBackend)
|
|
|
|
def updateMode(self, nameAsString):
|
|
assert nameAsString in constantsAndConfigs.availableEditModes
|
|
|
|
for track in self.tracks.values():
|
|
track.updateMode(nameAsString)
|
|
|
|
self.grid.updateMode(nameAsString)
|
|
|
|
def updateModeSingleTrackRedraw(self, nameAsString:str, trackId:int, trackExport:tuple):
|
|
"""trackExport is a tuple of block export dicts"""
|
|
self.tracks[trackId].updateMode(nameAsString)
|
|
#if nameAsString == "block":
|
|
# self.tracks[trackId].stretchXCoordinates(0.25) #go into small scaling mode. 0.25 is hardcoded and the same as scoreView.updateMode
|
|
|
|
def maxTrackLength(self):
|
|
if self.tracks:
|
|
return max(tr.lengthInPixel for tr in self.tracks.values())
|
|
#return max(max(tr.lengthInPixel for tr in self.tracks.values()), self.parentView.geometry().width())
|
|
#return max(max(tr.lengthInPixel for tr in self.scene().tracks.values()), self.scene().parentView.geometry().width()) + self.scene().parentView.geometry().width()
|
|
else:
|
|
return 0 #self.parentView.geometry().width()
|
|
|
|
def updateSceneRect(self):
|
|
self.parentView.setSceneRect(QtCore.QRectF(-5, self.yStart, self.maxTrackLength() + 300, self.cachedSceneHeight)) #x,y,w,h
|
|
|
|
def updateTrack(self, trackId, staticRepresentationList):
|
|
"""for callbacks"""
|
|
if trackId in self.tracks:
|
|
self.tracks[trackId].redraw(staticRepresentationList)
|
|
#else:
|
|
#hidden track. But this can still happen through the data editor
|
|
self.grid.reactOnScoreChange()
|
|
self.updateSceneRect()
|
|
|
|
def trackPaintBlockBackgroundColors(self, trackId, staticBlocksRepresentation):
|
|
if trackId in self.tracks:
|
|
self.tracks[trackId].paintBlockBackgroundColors(staticBlocksRepresentation)
|
|
#else:
|
|
#hidden track.
|
|
|
|
def updateGraphTrackCC(self, trackId, ccNumber, staticRepresentationList):
|
|
"""TrackId is a real notation track which has a dict of CCs"""
|
|
if trackId in self.tracks:
|
|
track = self.tracks[trackId]
|
|
ccPath = track.ccPaths[ccNumber]
|
|
ccPath.createGraphicItemsFromData(staticRepresentationList)
|
|
|
|
def updateGraphBlockTrack(self, trackId, ccNumber, staticRepresentationList):
|
|
"""TrackId is a real notation track which has a dict of CCs"""
|
|
if trackId in self.tracks:
|
|
self.tracks[trackId].ccPaths[ccNumber].updateGraphBlockTrack(staticRepresentationList)
|
|
|
|
def toggleNoteheadsRectangles(self):
|
|
for track in self.tracks.values():
|
|
track.toggleNoteheadsRectangles()
|
|
|
|
def syncCCsToBackend(self, trackId, listOfCCsInThisTrack):
|
|
"""Delete ccGraphs that are gui-only,
|
|
create gui versions of graphs that are backend-only.
|
|
Don't touch the others.
|
|
|
|
This is the entry point for new CCPaths. They get created
|
|
only here.
|
|
"""
|
|
#TODO: and moved from one CC value to another?
|
|
guiCCs = set(self.tracks[trackId].ccPaths.keys())
|
|
|
|
for backendCC in listOfCCsInThisTrack:
|
|
if backendCC in guiCCs:
|
|
guiCCs.remove(backendCC) #all right. no update needed.
|
|
else: #new CC. not existent in the Gui yet. Create.
|
|
#This is the place where we create new CCPaths
|
|
new = CCPath(parentGuiTrack = self.tracks[trackId], parentDataTrackId = trackId)
|
|
self.tracks[trackId].ccPaths[backendCC] = new #store in the GUI Track
|
|
new.setParentItem(self.tracks[trackId])
|
|
new.setPos(0,0)
|
|
#new.setZValue(100)
|
|
|
|
#all items left in the set are GUI-only CCs. which means the backend graphs were deleted. Delete them here as well.
|
|
for cc in guiCCs:
|
|
self.removeWhenIdle(self.tracks[trackId].ccPaths[cc])
|
|
del self.tracks[trackId].ccPaths[cc]
|
|
|
|
|
|
def redraw(self, listOfStaticTrackRepresentations):
|
|
"""The order of guiTracks depends on the backend index.
|
|
This way it is a no-brainer, we don't need to maintain our own
|
|
order, just sync with the backend when the callback comes in.
|
|
|
|
Also handles meta data like track names.
|
|
But not the actual track content, which is done
|
|
through self.updateTrack which has its own api-callback
|
|
|
|
called by callbacksDatabase.tracksChanged"""
|
|
|
|
for track in self.tracks.values():
|
|
track.hide()
|
|
|
|
doubleTrackOffset = 0
|
|
for trackExportObject in listOfStaticTrackRepresentations:
|
|
if not trackExportObject["id"] in self.tracks:
|
|
guiTrack = GuiTrack(self, trackExportObject)
|
|
self.tracks[trackExportObject["id"]] = guiTrack
|
|
self.addItem(guiTrack)
|
|
guiTrack.secondStageInitNowThatWeHaveAScene()
|
|
|
|
self.tracks[trackExportObject["id"]].staticExportItem = trackExportObject
|
|
self.tracks[trackExportObject["id"]].setPos(0, constantsAndConfigs.trackHeight * trackExportObject["index"] + doubleTrackOffset)
|
|
self.tracks[trackExportObject["id"]].setZValue(0) #direct comparison only possible with the grid, which is at -50
|
|
self.tracks[trackExportObject["id"]].nameGraphic.setText(trackExportObject["name"])
|
|
self.tracks[trackExportObject["id"]].show()
|
|
|
|
if trackExportObject["double"]:
|
|
doubleTrackOffset += constantsAndConfigs.trackHeight
|
|
|
|
toDelete = []
|
|
for trackId, track in self.tracks.items():
|
|
if not track.isVisible():
|
|
toDelete.append((trackId, track))
|
|
|
|
for trackId_, track_ in toDelete:
|
|
self.removeWhenIdle(track_)
|
|
del self.tracks[trackId_]
|
|
|
|
#Finally, under the last track, tell the user how many hidden tracks there are
|
|
nrOfHiddenTracks = len(api.session.data.hiddenTracks)
|
|
if nrOfHiddenTracks:
|
|
self.hiddenTrackCounter.setText("… and {} hidden tracks".format(nrOfHiddenTracks))
|
|
else: #remove previous status message
|
|
self.hiddenTrackCounter.setText("")
|
|
|
|
belowLastTrack = constantsAndConfigs.trackHeight * (trackExportObject["index"] + 1) + doubleTrackOffset
|
|
self.hiddenTrackCounter.setPos(5, belowLastTrack)
|
|
self.cachedSceneHeight = belowLastTrack + constantsAndConfigs.trackHeight
|
|
|
|
def removeWhenIdle(self, item):
|
|
"""Call this function instead of removeItem. You are responsible to delete the item from any
|
|
list or other container where it was stored in the meantime yourself.
|
|
|
|
Other methods tried:
|
|
-removeItem much slower
|
|
-create second scene and do scene2.addItem to get it out here - same as removeItem, if not slower
|
|
-delete a track and recreate a new one - same as all removeItems
|
|
-just hide the track and create a new one. slightly slower as hiding just the items.
|
|
-item.setFlag(QtWidgets.QGraphicsItem.ItemHasNoContents) slow, but not as slow as removeItem
|
|
-just hide items, never delete them is fast but gets slower the more items are hidden in the track
|
|
"""
|
|
item.hide()
|
|
self.deleteOnIdleStack.add(item) #This will actually delete the item and remove it from the scene when there is idle time
|
|
|
|
def _deleteOnIdle(self):
|
|
"""Hiding a QGraphicsItem is much faster than removing it from the scene. To keep the
|
|
GUI responsive but also to get rid of the old data we hide in createGraphicItemsFromData
|
|
and whenever there is time the actual removing and deleting will happen here.
|
|
|
|
This function is connected to a timer defined in self init.
|
|
"""
|
|
#if self.deleteOnIdleStack:
|
|
# deleteMe = self.deleteOnIdleStack.pop()
|
|
# self.removeItem(deleteMe) #This is the only line in the program that should call scene.removeItem
|
|
# del deleteMe
|
|
for deleteMe in self.deleteOnIdleStack:
|
|
self.removeItem(deleteMe) #This is the only line in the program that should call scene.removeItem
|
|
self.deleteOnIdleStack = set()
|
|
|
|
|
|
def trackAt(self, qScenePosition):
|
|
"""trackAt always returns the full GuiTrack, even if in ccEdit mode."""
|
|
|
|
if qScenePosition.y() < self.conductor.y() + self.conductor.totalHeight/4:
|
|
return self.conductor
|
|
|
|
for guiTrack in sorted(self.tracks.values(), key = lambda gT: gT.y()):
|
|
if guiTrack.y() >= qScenePosition.y() - constantsAndConfigs.trackHeight/2:
|
|
return guiTrack
|
|
else:
|
|
return None #no track here.
|
|
|
|
def blockAt(self, qScenePosition):
|
|
track = self.trackAt(qScenePosition)
|
|
if track is self.conductor:
|
|
return self.conductor.blockAt(qScenePosition.x())
|
|
if self.parentView.mode() in ("block", "notation"):
|
|
if track:
|
|
return track.blockAt(qScenePosition.x())
|
|
elif self.parentView.mode() == "cc":
|
|
if track and constantsAndConfigs.ccViewValue in track.ccPaths:
|
|
return track.ccPaths[constantsAndConfigs.ccViewValue].blockAt(qScenePosition.x())
|
|
else:
|
|
raise NotImplementedError
|
|
return None
|
|
|
|
|
|
def notTrueAnymore_wheelEvent(self, event):
|
|
"""
|
|
This docstring was either wrong in the first place or something changed in my code.
|
|
However, we do not need to eat wheelEvent here anymore. On the contrary: It will block
|
|
items, like the tempoItem from receiving wheelEvents.
|
|
The commented out if/else for item detection also has a chance to work, but we don't need
|
|
to test for that at all. Standard Qt-behavoiour is fine.
|
|
What follows is the old docstring, for legacy documentation reasons:
|
|
|
|
We MUST handle the event somehow. Otherwise background grid items will block the views(!)
|
|
wheel scrolling, even when disabled and setting accepting mouse events to none.
|
|
This is a qt bug that won't be fixed because API stability over correctnes (according to the
|
|
bugtracker.
|
|
|
|
Contrary to other parts of the system event.ignore and accept actually mean something.
|
|
ignore will tell the caller to use the event itself, e.g. scroll.
|
|
|
|
This event gets the wheel BEFORE the main window (zoom)
|
|
"""
|
|
#item = self.itemAt(event.scenePos(), self.parentView.transform())
|
|
#if type(item) is items.Note:
|
|
#super().wheelEvent(event) #send to child item
|
|
#else:
|
|
event.ignore() #so the view scrolls or we zoom
|
|
|
|
|
|
def stretchXCoordinates(self, factor):
|
|
"""Reposition the items on the X axis.
|
|
Call goes through all parents/children, starting from ScoreView._stretchXCoordinates.
|
|
Docstring there."""
|
|
#The big structures have a fixed position at (0,0) and move its child items, like notes, internally
|
|
#Some items, like the cursor, move around, as a whole item, in the scene directly and need no stretchXCoordinates() themselves.
|
|
self.grid.stretchXCoordinates(factor)
|
|
self.conductor.stretchXCoordinates(factor)
|
|
self.cursor.setX(self.cursor.pos().x() * factor)
|
|
self.playhead.setX(self.playhead.pos().x() * factor)
|
|
self.selection.stretchXCoordinates(factor)
|
|
|
|
for track in self.tracks.values():
|
|
track.stretchXCoordinates(factor)
|
|
|
|
self.updateSceneRect()
|
|
|
|
#Macro-Structure: Score / Track / Block Moving and Duplicating
|
|
#Hold the shift Key to unlock the moving mode.super().keyPressEvent(event)
|
|
#No note-editing requires a mouse action, so the mouse is free for controlling other aspects.
|
|
#Like zooming or moving blocks around.
|
|
|
|
"""
|
|
def keyPressEvent(self, event):
|
|
#Triggers only if there is no shortcut for an action.
|
|
modifiers = QtWidgets.QApplication.keyboardModifiers()
|
|
if modifiers == QtCore.Qt.AltModifier:
|
|
Alt
|
|
|
|
super().keyPressEvent(event)
|
|
"""
|
|
|
|
def mousePressEvent(self, event):
|
|
"""Pressing the mouse button is the first action of drag
|
|
and drop. We make the mouse cursor invisible so the user
|
|
can see where the object is moving
|
|
"""
|
|
if self.parentView.mode() == "block": #CC Block Moving is done in its own if clause below.
|
|
if event.button() == QtCore.Qt.LeftButton:
|
|
modifiers = QtWidgets.QApplication.keyboardModifiers()
|
|
|
|
#Block Move
|
|
if modifiers == QtCore.Qt.NoModifier:
|
|
block = self.blockAt(event.scenePos())
|
|
if block: #works for note blocks and conductor blocks
|
|
block.staticExportItem["guiPosStart"] = block.pos() #without shame we hijack the backend-dict.
|
|
self.duringBlockDragAndDrop = block
|
|
block.mousePressEventCustom(event)
|
|
|
|
#Track Move
|
|
elif (modifiers == QtCore.Qt.AltModifier or modifiers == QtCore.Qt.ShiftModifier ) and self.parentView.mode() == "block":
|
|
track = self.trackAt(event.scenePos())
|
|
if track and not track is self.conductor:
|
|
#self.parentView.setCursor(QtCore.Qt.BlankCursor)
|
|
self.cursor.hide()
|
|
track.staticExportItem["guiPosStart"] = track.pos()
|
|
self.duringTrackDragAndDrop = track
|
|
|
|
elif event.button() == QtCore.Qt.RightButton:
|
|
"""Fake context menu event. The built-in one never got
|
|
the correct detection. Yes, this will hardcode the
|
|
context menu to right mouse button, which is not really
|
|
the only way to trigger a context menu. However, too
|
|
much time has been spent to get this right already.
|
|
Good enough is better than good."""
|
|
block = self.blockAt(event.scenePos())
|
|
if block: #works for note blocks and conductor blocks
|
|
block.contextMenuEventCustom(event)
|
|
event.accept() #eat it
|
|
return
|
|
|
|
#CC is a special case because we need to handle points and block movement at the same time.
|
|
#Hence the alt/shift key is mandatory here. That was also the reason why everything was with the middle mouse button AND shortcuts once,
|
|
#but that was too inconvenient overall. This system is not 100% straightforward but easy enough to remember. And more robust against accidents.
|
|
elif self.parentView.mode() == "cc" and event.button() == QtCore.Qt.LeftButton:
|
|
modifiers = QtWidgets.QApplication.keyboardModifiers()
|
|
if (modifiers == QtCore.Qt.AltModifier or modifiers == QtCore.Qt.ShiftModifier):
|
|
block = self.blockAt(event.scenePos())
|
|
if block and type(block) is CCGraphTransparentBlock:
|
|
block.staticExportItem["guiPosStart"] = block.pos() #without shame we hijack the backend-dict.
|
|
self.duringBlockDragAndDrop = block
|
|
block.mousePressEventCustom(event)
|
|
event.accept() #eat it
|
|
return
|
|
|
|
super().mousePressEvent(event)
|
|
|
|
|
|
def _mmE_ensureCursorVisible(self, event):
|
|
"""Helper function for mouseMoveEvent.
|
|
|
|
centerOn and ensureVisible either crash or they recurse forever or they scroll exponentially
|
|
We must do it ourselves. Thanks, qt."""
|
|
if self.mouseMoveEventBlocked:
|
|
return #block the mouseMoveEvent recursion signal. If we scroll it sees the mouse as moving.
|
|
|
|
currentlyVisibleSceneRect = self.parentView.mapToScene(self.parentView.viewport().geometry()).boundingRect() #x, y top left corner, width, height
|
|
leftEdge = int(currentlyVisibleSceneRect.x())
|
|
rightEdge = int(leftEdge + currentlyVisibleSceneRect.width())
|
|
topEdge = int(currentlyVisibleSceneRect.y())
|
|
bottomEdge = int(topEdge + currentlyVisibleSceneRect.height())
|
|
x = int(event.scenePos().x())
|
|
y = int(event.scenePos().y())
|
|
|
|
if x < leftEdge:
|
|
self.mouseMoveEventBlocked = True #block the mouseMoveEvent recursion signal. If we scroll it sees the mouse as moving.
|
|
self.parentView.horizontalScrollBar().setValue(self.parentView.horizontalScrollBar().value() - (leftEdge - x))
|
|
elif x > rightEdge:
|
|
self.mouseMoveEventBlocked = True #block the mouseMoveEvent recursion signal. If we scroll it sees the mouse as moving.
|
|
self.parentView.horizontalScrollBar().setValue(self.parentView.horizontalScrollBar().value() + (x - rightEdge))
|
|
|
|
if y < topEdge:
|
|
self.mouseMoveEventBlocked = True #block the mouseMoveEvent recursion signal. If we scroll it sees the mouse as moving.
|
|
self.parentView.verticalScrollBar().setValue(self.parentView.verticalScrollBar().value() - (topEdge - y))
|
|
elif y > bottomEdge:
|
|
self.mouseMoveEventBlocked = True #block the mouseMoveEvent recursion signal. If we scroll it sees the mouse as moving.
|
|
self.parentView.verticalScrollBar().setValue(self.parentView.verticalScrollBar().value() + (y - bottomEdge))
|
|
|
|
self.mouseMoveEventBlocked = False
|
|
|
|
def mouseMoveEvent(self, event):
|
|
"""Catches certain mouse events for moving tracks and blocks.
|
|
Otherwise the event is propagated to the real QGraphicsItem.
|
|
Don't forget that an item needs to have the flag movable or selectable or else
|
|
it will not get mouseRelease or mouseMove events. MousePress always works.
|
|
|
|
Only active when mouse was pressed!
|
|
"""
|
|
|
|
self._mmE_ensureCursorVisible(event) #Works with all buttons. Good.
|
|
|
|
if self.duringTrackDragAndDrop:
|
|
#X is locked for tracks.
|
|
#self._mmE_ensureCursorVisible(event)
|
|
x = self.duringTrackDragAndDrop.staticExportItem["guiPosStart"].x()
|
|
y = event.scenePos().y()
|
|
self.duringTrackDragAndDrop.setPos(x, y)
|
|
|
|
elif self.duringBlockDragAndDrop:
|
|
#GuiBlockHandles are children of a Track. Their Y position will be offset by the track when we move them around with the mouse.
|
|
#Which is the same the distance to the first track is on top. We need to substract that for a good look and feel.
|
|
#The actual later dropping of the block is handled with the mouse cursor coordinates, the moving colored block is just eyecandy.
|
|
#TODO: I want to scroll but this does not work because the item is wider than the viewport. It actually crashes qt
|
|
#self._mmE_ensureCursorVisible(event)
|
|
x = event.scenePos().x()
|
|
y = event.scenePos().y() - self.duringBlockDragAndDrop.parentGuiTrack.y()
|
|
self.duringBlockDragAndDrop.setPos(x, y)
|
|
else:
|
|
super().mouseMoveEvent(event)
|
|
# #this happens EVERY mouse move. Just idle mouse movement over the window.
|
|
|
|
|
|
|
|
def mouseReleaseEvent(self, event):
|
|
"""Catches certain mouse events for moving tracks and blocks.
|
|
Otherwise the event is propagated to the real QGraphicsItem.
|
|
Don't forget that an item needs to have the flag movable or selectable or else
|
|
it will not get mouseRelease or mouseMove events. MousePress always works."""
|
|
|
|
#self.parentView.unsetCursor() #While moving stuff the mouse-cursor is hidden. Reset.
|
|
#self.cursor.show() #Our own position cursor #TODO: why was that in here? It shows the cursor after a mouseclick, even in CC mode (it should not)
|
|
tempBlockDragAndDrop = self.duringBlockDragAndDrop
|
|
tempTrackDragAndDrop = self.duringTrackDragAndDrop
|
|
self.duringBlockDragAndDrop = None
|
|
self.duringTrackDragAndDrop = None
|
|
#Now we can exit the function safely at any time without the need to reset the dragAndDrop storage in different places
|
|
|
|
if tempTrackDragAndDrop: #This is only for GuiTrack aka note tracks. CC tracks follow the gui track and the conductor cannot be moved.
|
|
assert not tempBlockDragAndDrop
|
|
assert type(tempTrackDragAndDrop) is GuiTrack
|
|
dragTrackPosition = tempTrackDragAndDrop.pos().y()
|
|
trackPositions = sorted([guiTrack.pos().y() for id, guiTrack in self.tracks.items()])
|
|
trackPositions.remove(dragTrackPosition)
|
|
|
|
listOfTrackIds = api.session.data.asListOfTrackIds()
|
|
listOfTrackIds.remove(tempTrackDragAndDrop.staticExportItem["id"])
|
|
|
|
#Calculate the new track position and trigger a redraw
|
|
canditateForNewTrackPosition = 0 #this takes care of a score with only one track and also if you move a track to the 0th position
|
|
for y in trackPositions:
|
|
if dragTrackPosition >= y:
|
|
canditateForNewTrackPosition = trackPositions.index(y) +1
|
|
|
|
listOfTrackIds.insert(canditateForNewTrackPosition, tempTrackDragAndDrop.staticExportItem["id"])
|
|
if len(listOfTrackIds) == 1:
|
|
tempTrackDragAndDrop.setPos(tempTrackDragAndDrop.staticExportItem["guiPosStart"]) #no need to trigger an api call with undo history etc.
|
|
else:
|
|
api.rearrangeTracks(listOfTrackIds)
|
|
|
|
elif tempBlockDragAndDrop: #CC blocks, note blocks and conductor blocks
|
|
assert not tempTrackDragAndDrop
|
|
tempBlockDragAndDrop.mouseReleaseEventCustom(event)
|
|
targetTrack = self.trackAt(event.scenePos()) #this ALWAYS returns a Conductor, GuiTrack or None. Not a CC sub-track. for CC see below when we test the block type and change this variable.
|
|
targetBlock = self.blockAt(event.scenePos())
|
|
dragBlockId = tempBlockDragAndDrop.staticExportItem["id"]
|
|
|
|
#First some basic checks:
|
|
if not targetTrack: #Only drag and drop into tracks.
|
|
return None
|
|
if targetBlock is tempBlockDragAndDrop: #block got moved on itself
|
|
return None
|
|
|
|
|
|
#Now check what kind of block moving we are dealing with
|
|
#If the drag and drop mixes different block/track types we exit
|
|
blockType = type(tempBlockDragAndDrop)
|
|
conductorBlock = noteBlock = ccBlock = False
|
|
|
|
if blockType is GuiBlockHandle:
|
|
if type(targetTrack) is GuiTrack and self.parentView.mode() in ("block", "notation"):
|
|
noteBlock = True
|
|
else: #Drag and Drop between different track types.
|
|
return None
|
|
|
|
elif blockType is ConductorTransparentBlock:
|
|
if targetTrack is self.conductor:
|
|
conductorBlock = True
|
|
else: #Drag and Drop between different track types.
|
|
return None
|
|
|
|
elif blockType is CCGraphTransparentBlock:
|
|
if (not type(targetTrack) is GuiTrack) or (not self.parentView.mode() == "cc"):
|
|
return None
|
|
|
|
ccBlock = True
|
|
if constantsAndConfigs.ccViewValue in targetTrack.ccPaths: #this is only the backend database of CCs in tracks but it should be in sync.
|
|
targetTrackId = targetTrack.staticExportItem["id"] #we need this later. save it before changing the targetBlock variable
|
|
targetTrack = targetTrack.ccPaths[constantsAndConfigs.ccViewValue]
|
|
else:
|
|
return None #TODO: Create a new CC sub-track with the moved block as first block. Mainly a backend call. Needs a call at the end of this function as well.
|
|
|
|
else:
|
|
raise TypeError("Block must be a conductor, note or CC type but is {}".format(blockType))
|
|
|
|
#We now have a track and the track type matches the block type.
|
|
#Find the position to insert.
|
|
if targetBlock is None: #behind the last block
|
|
positionToInsert = len(targetTrack.transparentBlockHandles) #essentially we want append() but insert() is compatible with all types of operation here. len is based 1, so len results in "after the last one" position.
|
|
else:
|
|
assert type(targetBlock) is blockType
|
|
positionToInsert = targetTrack.transparentBlockHandles.index(targetBlock)
|
|
assert positionToInsert >= 0
|
|
|
|
#Create the new order by putting the old block into a new slot and removing it from its old position
|
|
newBlockOrder = [guiBlock.staticExportItem["id"] for guiBlock in targetTrack.transparentBlockHandles]
|
|
if targetTrack == tempBlockDragAndDrop.parent:
|
|
newBlockOrder.remove(dragBlockId) #block will be at another position and removed from its old one.
|
|
newBlockOrder.insert(positionToInsert, dragBlockId)
|
|
|
|
#Finally call the appropriate backend function which will trigger a GuiUpdate.
|
|
if targetTrack is tempBlockDragAndDrop.parent: #Same track or within the conductor track
|
|
if conductorBlock:
|
|
assert targetTrack is self.conductor
|
|
api.rearrangeTempoBlocks(newBlockOrder)
|
|
elif noteBlock:
|
|
api.rearrangeBlocks(targetTrack.staticExportItem["id"], newBlockOrder)
|
|
elif ccBlock:
|
|
api.rearrangeCCBlocks(targetTrackId, constantsAndConfigs.ccViewValue, newBlockOrder)
|
|
#else is already catched by a Else-TypeError above
|
|
else: #Different Track,
|
|
if conductorBlock or targetTrack is self.conductor:
|
|
raise RuntimeError("How did this slip through? Checking for cross-track incompatibility was already done above")
|
|
elif noteBlock:
|
|
api.moveBlockToOtherTrack(dragBlockId, targetTrack.staticExportItem["id"], newBlockOrder)
|
|
elif ccBlock: #TODO: Also different CC in the same GuiTrack
|
|
api.moveCCBlockToOtherTrack(dragBlockId, targetTrack.parentDataTrackId, newBlockOrder)
|
|
|
|
elif self.parentView.mode() == "notation" and (not (tempBlockDragAndDrop or tempTrackDragAndDrop)) and event.button() == 1: #a positional mouse left click in a note-track, but only it not drag-drop release.
|
|
track = self.trackAt(event.scenePos())
|
|
if track and not track is self.conductor:
|
|
modifiers = QtWidgets.QApplication.keyboardModifiers()
|
|
trackId = track.staticExportItem["id"]
|
|
if modifiers == QtCore.Qt.ShiftModifier:
|
|
api.selectToTickindex(trackId, event.scenePos().x() * constantsAndConfigs.ticksToPixelRatio)
|
|
else:
|
|
api.toTickindex(trackId, event.scenePos().x() * constantsAndConfigs.ticksToPixelRatio)
|
|
|
|
super().mouseReleaseEvent(event)
|
|
|